<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>zemlan.in</title>
  <description>zemlan.in</description>
  <link>https://zemlan.in/</link>
  <pubDate>Tue, 10 Dec 2024 18:14:26 GMT</pubDate>
  <ttl>1800</ttl>
  <atom:link href="https://zemlan.in/rss.xml" rel="self" type="application/rss+xml" />
    <atom:link rel="hub" href="https://pubsubhubbub.appspot.com/" />

    <item>
      <title>On Code Reviews</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;dQmCL9NHbmILsnznv7Y4jAs4YD.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The code is done, now to the hard part&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Maybe, I’m the only one, but I’m annoyed by jokes like this:&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;blockquote cite&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;
        10 lines of code &#x3D; 10 issues.&lt;br&gt;&lt;br&gt;500 lines of code &#x3D; &quot;looks fine.&quot;&lt;br&gt;&lt;br&gt;Code reviews.
      &lt;&#x2F;blockquote&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;&lt;b&gt;I Am Devloper on Twitter&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I mean, the joke by itself is fine — &lt;code&gt;i-know-that-feel.png&lt;&#x2F;code&gt;, &lt;code&gt;#relatable&lt;&#x2F;code&gt;, all that. But it contains several examples of a bad behavior both of code authors and of code reviewers:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;surface-level comments;&lt;&#x2F;li&gt;
&lt;li&gt;huge patches;&lt;&#x2F;li&gt;
&lt;li&gt;lack of context;&lt;&#x2F;li&gt;
&lt;li&gt;skimming&#x2F;reading “diagonally”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So, if code review problems are &lt;em&gt;so&lt;&#x2F;em&gt; widespread, then we should not just point at them, but also try to address them somehow. One could do that either via external requirements&#x2F;limitations or via internal motivation&lt;&#x2F;p&gt;
&lt;p&gt;To require “be better” is dumb. Automatically rejecting code review because of metrics — not an option, because “understandability” can’t be measured by a script. So we’ll disregard “improve code reviews via external requirements&#x2F;limitations”&lt;&#x2F;p&gt;
&lt;p&gt;One could influence another’s internal motivation by an example, but after years of attempts, this seems too slow and not that reliable. Maybe, a personal example can be better communicated in a post…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Aside&lt;&#x2F;em&gt;: this post was originally written as &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;a&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;series&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;of&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;posts&lt;&#x2F;a&gt; in Ukrainian in the beginning of 2021. Many things have changed since then (like I&#39;m currently working in a smaller company and a team than in 2021), but the general message and advice still seems relevant (albeit somewhat meandering)&lt;&#x2F;p&gt;
&lt;p&gt;Also, to make writing process easier, this post pretends as if Git and Github are the only ways to collaborate while writing code. If you’re lucky enough to use Fossil or Phabricator or Gitea or whatever… Okay…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;what-are-code-reviews-for&quot;&gt;What Are Code Reviews For?&lt;&#x2F;h2&gt;
&lt;h3 id&#x3D;&quot;time-and-place&quot;&gt;Time and Place&lt;&#x2F;h3&gt;
&lt;p&gt;I’ve &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;anton.codes&#x2F;#jobs&quot;&gt;only worked&lt;&#x2F;a&gt; in rather mature product companies. When a company works on a products for years, the time and effort investment into code review is justified not only because “that’s a correct thing to do”, but also because it’ll make future support easier&lt;&#x2F;p&gt;
&lt;p&gt;Really young companies, who might never get to that “future”, are taking a risk that, after all those reviews, they’ll remain with a “correct” but useless code&lt;&#x2F;p&gt;
&lt;p&gt;I don’t &lt;em&gt;know&lt;&#x2F;em&gt; how it is to work at an agency&#x2F;outsource&#x2F;outstaff. On one hand, if a client will return for a follow-up work, previous code reviews should help to remember the codebase faster. On the other, client might never return&lt;&#x2F;p&gt;
&lt;p&gt;So, code reviews might be completely useless (or even harmful) on your current workplace. Or they were on your new colleague’s previous workplace…&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h3&gt;
&lt;p&gt;Okay, let’s assume you’re working in a relevant team, at a relevant time, and you really want to get better at writing and commenting pull requests. To know the direction of improvements, you’ll need to understand what’s the goal of code reviews&lt;&#x2F;p&gt;
&lt;p&gt;So, what’s the goal of code reviews?&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I was told that’s what we do here&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;“Due to historical reasons” is a bad path to improvements. It’ll lead you to where you’ve started. There even won’t be a story to tell (unless you like stories where nothing happens)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;So that idiots won’t break anything in my perfect project&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Those “idiots” have opened a PR because they need to fix or to improve something. If your perfect project is holding on together only thanks to your gatekeeping, you should invest some time into writing tests and setting up linters&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;To catch what’s difficult or impossible to cover with tests&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Warmer… But “opinions leaders” will soon get tired of staring at hundreds and thousands of line of code, looking for the same problems that trip colleagues unfamiliar with the repo&lt;&#x2F;p&gt;
&lt;p&gt;At the same time, novices of the repo (the repo specifically — they might have a lot of experience outside of it) might not know that &lt;code&gt;{placeholder}&lt;&#x2F;code&gt; will create problems, while experts have encountered &lt;code&gt;{placeholder}&lt;&#x2F;code&gt; so many times that they instinctively avoid it&lt;&#x2F;p&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;EfD2pNDO8InhgJg7tNqMhl2Pgn&#x2F;gifv.mp4&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;p&gt;So, the goal of code reviews is to let authors and reviewers to share their understanding of the project with each other:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;authors, explicitly and implicitly, point out weirdness in existing code. For example, the fact that to achieve a simple task, they need to do a lot of manual&#x2F;boilerplate work&lt;&#x2F;li&gt;
&lt;li&gt;reviewers share and communicate project’s foundational assumptions, stuff like “on errors, you’ll need to expect HTTP 200 with a &lt;code&gt;error&lt;&#x2F;code&gt; key because that was a decision made long ago and around which &lt;em&gt;a lot&lt;&#x2F;em&gt; of code was written”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In other words, you need two to &lt;del&gt;tango&lt;&#x2F;del&gt; have a good code review&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;opening-a-pull-request&quot;&gt;Opening a Pull Request&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;NyVRViO3aJO44aRjXyFYNGjj7z&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The Repo With A Thousand Faces&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When writing a pull request (&lt;em&gt;PR&lt;&#x2F;em&gt;), it’s useful to be guided by the idea of “invest as much time and&#x2F;or effort as you want others will invest into reviewing”&lt;&#x2F;p&gt;
&lt;p&gt;If the author won’t invest time into gathering everything in a nice package, then reviewers would need to do that themselves. Even if reviewers &lt;em&gt;know&lt;&#x2F;em&gt; about a project’s quirk, they might not remember about it when reading the PR, because their mind is busy with connecting the dots that author decided not to connect&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;github-stories&quot;&gt;Github Stories&lt;&#x2F;h3&gt;
&lt;p&gt;If the goal of code reviews is to share and get others’ thoughts and opinions, first your colleagues need to understand what’s happening. Knowing that, you might arrive at a bad and a good conclusion:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;a bad conclusion: “if colleagues understand — great, if not — that’s their problem”&lt;&#x2F;li&gt;
&lt;li&gt;a good conclusion: “I can make PR less or more understandable”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;To make it easier for colleagues to understand a PR and to share their relevant experience, they need to know “how it was”, “what’s changing”, “why it has to change”, “how it changes”, and “what it becomes”. Changes in the code itself tells about some of these&lt;&#x2F;p&gt;
&lt;p&gt;For example, the left&#x2F;red part of a diff tells us “how it was”, while the right&#x2F;green one — “what it becomes”. The rest of the questions we’ll need to address in prose&lt;&#x2F;p&gt;
&lt;p&gt;If that prose is a collection of dry facts, then reviewers would have to connect them themselves. To make it easier, PR’s author can unite these facts into &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hero%27s_journey&quot;&gt;a transformation story&lt;&#x2F;a&gt; in which PR’s repo is the main character&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;RJNOM2DlZbru3RZPoahAhraT2Y.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We understand stories and can “extract” their gist, because we’re listening&#x2F;reading&#x2F;watching them our whole lives. Plus, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.sciencedaily.com&#x2F;releases&#x2F;2020&#x2F;12&#x2F;201215131236.htm&quot;&gt;our brain perceives code as a puzzle&lt;&#x2F;a&gt;, so we might accidentally ignore consequences of a change — a puzzle is solved, the end, nothing will happen to it… Because of that, it makes sense to tell stories while writing a PR. You’ll have ample opportunity to so do&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:more-of-a-suggestion&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:more-of-a-suggestion&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;words-mean-things&quot;&gt;Words Mean Things&lt;&#x2F;h3&gt;
&lt;p&gt;You can start writing the tale long before pressing the “New pull request” button — in the code itself. Even without comments, you can describe “the main cast” in the &lt;strong&gt;names of files, functions, and variables&lt;&#x2F;strong&gt;. A good name of a thing will describe what and why it is, will show its connection with other parts of the code, and will help with discussing it&lt;&#x2F;p&gt;
&lt;p&gt;That’s why it is important to avoid generic names (like &lt;code&gt;utils&#x2F;data.js&lt;&#x2F;code&gt;), same names for unrelated things, and to pay attention to existing names — if feature is already called &lt;code&gt;wunderwaffle&lt;&#x2F;code&gt;, don’t begin suddenly calling it &lt;code&gt;terrifictiramisu&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;strong&gt;name of the branch&lt;&#x2F;strong&gt; will help you to focus the PR and to avoid distracting both you and reviewers with “extras”. For example, if you’re in the branch named &lt;code&gt;JD-48&#x2F;speed-up-uploads&lt;&#x2F;code&gt;, it’ll be easier to stop yourself from updating all dependencies of the JD project or from changing uploads in the ER project&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;the-medium-is-the-commit-message&quot;&gt;The Medium is the (Commit) Message&lt;&#x2F;h3&gt;
&lt;p&gt;If you’ve messed up and the branch already includes too many unrelated changes (which would still be helpful in this or another project), you can play with the &lt;strong&gt;content of the commits&lt;&#x2F;strong&gt;. Instead of including all of the file’s changes in the same commit, nothing stops you from adding changes line-by-line. Fans of CLI can do it with &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-add#Documentation&#x2F;git-add.txt---patch&quot;&gt;&lt;code&gt;git add --patch&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; or &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-add#Documentation&#x2F;git-add.txt---interactive&quot;&gt;&lt;code&gt;git add --interactive&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;; personally I got used to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.git-tower.com&#x2F;&quot;&gt;Tower’s&lt;&#x2F;a&gt; GUI for that&lt;&#x2F;p&gt;
&lt;p&gt;How to understand which lines belong to one commit, and which — to another? Think about where these lines happen in the PR’s “story” and describe that in the &lt;strong&gt;title of the commit&lt;&#x2F;strong&gt;, while limiting yourself to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;tbaggery.com&#x2F;2008&#x2F;04&#x2F;19&#x2F;a-note-about-git-commit-messages.html&quot;&gt;git-recommended 50 symbols&lt;&#x2F;a&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:50-plus&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:50-plus&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. Maybe, that story is a short anecdote and &lt;code&gt;fix A when B&lt;&#x2F;code&gt; will be enough. Maybe, it has multiple “characters” and you should first introduce them to the reader (&lt;code&gt;add X to do Y&lt;&#x2F;code&gt;), and then to each other (&lt;code&gt;use X in Z&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Describe your changes in imperative mood, e.g. «make xyzzy do frotz» instead of «[This patch] makes xyzzy do frotz» or «[I] changed xyzzy to do frotz», as if you are giving orders to the codebase to change its behavior.  Try to make sure your explanation can be understood without external resources. Instead of giving a URL to a mailing list archive, summarize the relevant points of the discussion.&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git.kernel.org&#x2F;pub&#x2F;scm&#x2F;git&#x2F;git.git&#x2F;tree&#x2F;Documentation&#x2F;SubmittingPatches?id&#x3D;HEAD#n136&quot;&gt;SubmittingPatches - The core git plumbing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Additionally, many projects have set up an integration between Git and their ticket manager, so that commits&#x2F;branches linked to tickets and vice versa. Because of that, it usually makes sense to mention the ticket number in the title of the commit, for example,  &lt;code&gt;#JD-48 cache responses from X&lt;&#x2F;code&gt;. Format (&lt;code&gt;XX-00&lt;&#x2F;code&gt;, &lt;code&gt;$XX-00&lt;&#x2F;code&gt;, &lt;code&gt;#XX-00&lt;&#x2F;code&gt;) depends on your project, but any prefix before numbers is enough for Github to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;administering-a-repository&#x2F;configuring-autolinks-to-reference-external-resources&quot;&gt;display ticket links in its UI&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;fIAjachuzUKWXol3Uc6L5KCeo4.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Many projects also follow &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.conventionalcommits.org&#x2F;&quot;&gt;conventional commit&lt;&#x2F;a&gt; and use tags like &lt;code&gt;feat&lt;&#x2F;code&gt; and &lt;code&gt;chore&lt;&#x2F;code&gt;. I personally think these tags are quite useless for a product without public CHANGELOG, but the habit of categorizing change might help with keeping PRs and commits focused&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;retelling-the-story&quot;&gt;Retelling the Story&lt;&#x2F;h3&gt;
&lt;p&gt;If you’ve confused the &lt;strong&gt;order of the commits&lt;&#x2F;strong&gt;, you can change it with &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-rebase#_interactive_mode&quot;&gt;&lt;code&gt;git rebase --interactive&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; which will make it easy to reorder&#x2F;rewrite&#x2F;combine&#x2F;delete altogether commits by editing a text file with a list of operations and commits&lt;&#x2F;p&gt;
&lt;p&gt;For example, you’ve been working in the &lt;code&gt;JD-48&lt;&#x2F;code&gt; branch, where you’ve made five commits. Local commit history currently looks like this (&lt;code&gt;git log --graph --oneline --all&lt;&#x2F;code&gt;, newer commits at the top) :&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-git&quot;&gt;* f4593f9 (HEAD -&amp;gt; JD-48) add tests for X-Y integration
* 04d0fda optimize Y
* ba46169 integrate X with Y
* fa1afe1 tests for X
* 5928aea setup X
* 300a500 (master, origin&#x2F;master, origin&#x2F;HEAD) …
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;git rebase --interactive &quot;5928aea^&quot;&lt;&#x2F;code&gt; will open these lines in your text editor&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:vi&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:vi&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;5928&lt;&#x2F;span&gt;aea setup &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick fa1afe&lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt; tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick ba&lt;span class&#x3D;&quot;hljs-number&quot;&gt;46169&lt;&#x2F;span&gt; integrate &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt; with &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;04&lt;&#x2F;span&gt;d0fda optimize &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick f4593f&lt;span class&#x3D;&quot;hljs-number&quot;&gt;9&lt;&#x2F;span&gt; add tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;-&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt; integration
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Each line is an operation to do when creating a new version of the branch. By default, rebase takes and applies every commit (&lt;code&gt;pick&lt;&#x2F;code&gt;). Additionally, you can skip a commit (&lt;code&gt;drop&lt;&#x2F;code&gt;), combine it with the earlier one (&lt;code&gt;squash&lt;&#x2F;code&gt;), apply it with an edit (&lt;code&gt;fixup&lt;&#x2F;code&gt;); you can reorder commits (by reordering lines in the file) and execute arbitrary shell-commands (&lt;code&gt;exec&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;For example, if you want to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;move the tests commit (&lt;code&gt;fa1afe1&lt;&#x2F;code&gt;) to the end of the branch;&lt;&#x2F;li&gt;
&lt;li&gt;combine it with the integration tests one (&lt;code&gt;f4593f9&lt;&#x2F;code&gt;);&lt;&#x2F;li&gt;
&lt;li&gt;and delete the optimizations of the unrelated part of the project (because this PR is about new feature, not optimizations)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;you’ll need to edit the “script” generated by &lt;code&gt;git rebase --interactive&lt;&#x2F;code&gt; to be that:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;5928&lt;&#x2F;span&gt;aea setup &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick ba&lt;span class&#x3D;&quot;hljs-number&quot;&gt;46169&lt;&#x2F;span&gt; integrate &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt; with &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
drop &lt;span class&#x3D;&quot;hljs-number&quot;&gt;04&lt;&#x2F;span&gt;d0fda optimize &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick fa1afe&lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt; tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
squash f4593f&lt;span class&#x3D;&quot;hljs-number&quot;&gt;9&lt;&#x2F;span&gt; add tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;-&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt; integration
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or you can stop boasting and just use your mouse to rewrite the history, for example in aforementioned &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.git-tower.com&#x2F;help&#x2F;guides&#x2F;commit-history&#x2F;interactive-rebase&#x2F;mac&quot;&gt;Tower&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After the branch and the commits are polished, it’s time to work on the &lt;strong&gt;title and the description of the PR&lt;&#x2F;strong&gt;. They are written, more or less, similarly to commits, but with more of a bird view&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;what-to-write-in-pull-requests-description&quot;&gt;What to Write in Pull Request’s Description?&lt;&#x2F;h3&gt;
&lt;p&gt;Usually, the most difficult part of the description for me is to begin. Blank &lt;del&gt;page&lt;&#x2F;del&gt; &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; blocks a lot of people&lt;&#x2F;p&gt;
&lt;p&gt;Some repos have &lt;code&gt;PULL_REQUEST_TEMPLATE.md&lt;&#x2F;code&gt;, which can help with writer’s block, but private projects frequently either lack it or it is rather useless as a prompt:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;&lt;span class&#x3D;&quot;hljs-section&quot;&gt;# Summary&lt;&#x2F;span&gt;
...

---

&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; [ ] &amp;lt;!-- some-continuous-integration-checkbox --&amp;gt; ci-bot, do something
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Titles and descriptions of commits also might help with the blank &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt;. If they are not enough, begin with “So, there’s &lt;code&gt;noun&lt;&#x2F;code&gt; and it is &lt;code&gt;verb&lt;&#x2F;code&gt;” and extend this intro with context&lt;&#x2F;p&gt;
&lt;p&gt;That context might include:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;causes for the change (less “i was asked by manager”, more “users have a problem”)&lt;&#x2F;li&gt;
&lt;li&gt;weird lines, where it &lt;em&gt;seems&lt;&#x2F;em&gt; you could do better, but you don’t know how and&#x2F;or too lazy to. Maybe, the lines aren’t weird at all, maybe your colleagues know a good approach, maybe somebody will motivate you to power through the laziness&lt;&#x2F;li&gt;
&lt;li&gt;links to related tickets and PRs; to the documentation of new&#x2F;strange functions; to the answer on StackOverflow from which you’ve copied that snippet…&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Speaking of links… Ten minutes learning Markdown&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:markdown-flavors&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:markdown-flavors&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; will help you with readability&#x2F;writability of the description. For example, compare how different link styles read:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color&lt;&#x2F;span&gt;),
but we [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;can’t use it&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color&lt;&#x2F;span&gt;) yet in all of
[&lt;span class&#x3D;&quot;hljs-string&quot;&gt;our supported browsers&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers&lt;&#x2F;span&gt;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s &lt;span class&#x3D;&quot;hljs-code&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;, but we can’t use it yet in all of our supported browsers

Links:
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;mdn&lt;&#x2F;span&gt;], but we [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;can’t use it&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;caniuse&lt;&#x2F;span&gt;] yet in all of [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;our supported browsers&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;kb&lt;&#x2F;span&gt;]

[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;mdn&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color&lt;&#x2F;span&gt;
[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;caniuse&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color&lt;&#x2F;span&gt;
[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;kb&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;pull-request-assignees&quot;&gt;Pull Request Assignees&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;0eSXpBAnhrHMabQo8qahYSXwBF&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;When all the prose is written, it needs an audience. And you should know your audience&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;you-are-reviewer-zero&quot;&gt;You are Reviewer Zero&lt;&#x2F;h3&gt;
&lt;p&gt;Author of the PR is the reviewer zero. Of course, nobody stops you from switching to another task immediately after you’ve passed the baton to your colleagues, but reading familiar code in another environment (in code review UI instead of the code editor one) or at another time (when all the “obviously” are forgotten) will help you notice an mistake or some weird thing on your own&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I sometimes do this as well – after publishing my PR, I review it myself (on GitHub, you can comment on your own PR, but without approving it). This allows me to add comments&#x2F;context specifically to the code that it&#39;s related to&lt;&#x2F;p&gt;
&lt;p&gt;— a good comment to the previous section&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;It’s especially comfortable to do in a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.blog&#x2F;2019-02-14-introducing-draft-pull-requests&#x2F;&quot;&gt;draft pull request&lt;&#x2F;a&gt;, which already kinda exists and has CI checks ran against, but hasn’t announced itself to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;creating-cloning-and-archiving-repositories&#x2F;about-code-owners&quot;&gt;code owners&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;who-to-assign-to&quot;&gt;Who to Assign to&lt;&#x2F;h3&gt;
&lt;p&gt;In small projects with a tight team, the question of “who to assign review to” answers itself — you either assign it to everyone (because there are not many of you), or you know the person responsible for this or that part of the codebase and assign it to them&lt;&#x2F;p&gt;
&lt;p&gt;In more… &lt;em&gt;dynamic&lt;&#x2F;em&gt; projects and teams, due to scale and worse communication, answering that question is more difficult. Often, it’s solved by including either whole teams or “opinion leaders” (team&#x2F;tech leads, senior engineers, etc.) in the code owners list. But either option creates its own problems&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;teams&quot;&gt;Teams&lt;&#x2F;h4&gt;
&lt;p&gt;Assigning a team risks making every team member think that reviewing your pull request is someone else’s task. Having a rotating “on call” person doesn’t save you — for example, if the pull request was opened Friday evening, when I was on call, Monday morning I’ll ignore it thinking that it’s now Julia’s problem because she’s on call today. At the same time, Julia might just as justifiably ignore it herself — pull request is from Friday, and on Friday she wasn’t on call&lt;&#x2F;p&gt;
&lt;p&gt;Plus, new team members might be too shy to comment — what if they won’t notice something or will ask a “stupid” question. If the PR is assigned to the whole team, they can just wait one out&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;NkgYr8RSbndFNZiYNCd4d7iqXy.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Assigning to a team might also have the reverse effect, when &lt;em&gt;everyone&lt;&#x2F;em&gt; is trying to quickly write a comment without going into details&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:tnx&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:tnx&quot; rel&#x3D;&quot;footnote&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. In theory, pull request’s author could spend time and effort to answer every single passerby, but it’s hard, exhausting, and might lead to conversations with management if it thinks that “your task is to solve problems with the code, not to mentor junior developers in other teams”&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;opinion-leaders&quot;&gt;Opinion leaders&lt;&#x2F;h4&gt;
&lt;p&gt;Having a dedicated person who reads through every pull request has its positives, of course, stuff like more consistent style and higher chance to notice a conflict with project’s implicit assumptions&lt;&#x2F;p&gt;
&lt;p&gt;But it’s absurdly to expect (or require) one person to thoughtfully go through everything at the same speed, with which the rest of the team writes code. It’s impossible to get all three in the “quantity&#x2F;speed&#x2F;quality” triangle. You’re lucky even if you’ll manage to get two…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;5iTt8tIMx5DBhGyJE4hQq9vKLt.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I don’t know who to decidedly solve issues of either&#x2F;both approaches. But I’m sure that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;it’s better to assign individual people instead of the whole team, even if their lone contribution will be re-assigning PR to someone more relevant&lt;&#x2F;li&gt;
&lt;li&gt;it’s very important to avoid creating a “bottleneck person”, who won’t last long under the pressure of superiors and peers&lt;&#x2F;li&gt;
&lt;li&gt;reviewing pull requests should not only leads&#x2F;senior people, but junior developers too. Otherwise, without junior’s “what’s happening here?”, everything will fly off on abstractions upon abstractions, and it’s easier to simplify code during code review than after it&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;code-review--response&quot;&gt;Code review &amp;amp; response&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Z0Ce7K9w3oLjSbi0XaXtQ1LeSi&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;So, your colleague has written and described the code, you’ve gotten a notification about her pull request. What’s next?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;to-listen&quot;&gt;To Listen&lt;&#x2F;h3&gt;
&lt;p&gt;Every pull request has reasons it exists. Aside from obvious external “fix a bug”, “add a feature”&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:6&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:6&quot; rel&#x3D;&quot;footnote&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, and “update dependencies”, the reason might be internal, for example:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;intermediate step to fix a bug&#x2F;add a feature (“this function is exported but never used here because it’ll be used it not-yet published code&#x2F;PR”)&lt;&#x2F;li&gt;
&lt;li&gt;align existing code with author’s mental model (“it was difficult for me to follow, I rewrote it so that it’s more understandable”)&lt;&#x2F;li&gt;
&lt;li&gt;improve the ease of development (“i use &lt;code&gt;grep&lt;&#x2F;code&gt;, but this file made it harder to do”)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Reviewers might disagree with pull request’s reasons or how they were handled, but for a good code review, they need to &lt;em&gt;know&lt;&#x2F;em&gt; them and to align them with project’s goals if necessary. So you should think about what reasons guided the author. For external reasons, you can look at the ticket description or background conversations. Internal ones can be deciphered from commit history or pull request’s description&lt;&#x2F;p&gt;
&lt;p&gt;If author didn’t leave you with much breadcrumbs, you can help yourself by &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;collaborating-with-issues-and-pull-requests&#x2F;filtering-files-in-a-pull-request&quot;&gt;filtering files&lt;&#x2F;a&gt;, looking &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;searching-for-information-on-github&#x2F;searching-commits#search-by-author-or-committer&quot;&gt;author’s past changes&lt;&#x2F;a&gt;, doing a local checkout of the branch, attempting to rewrite “a strange solution” with all your “this shouldn’t be like that”…&lt;&#x2F;p&gt;
&lt;p&gt;Or, as the &lt;em&gt;laaaaaast&lt;&#x2F;em&gt; resort, you can just ask them&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;to-speak&quot;&gt;To Speak&lt;&#x2F;h3&gt;
&lt;p&gt;Just like every change in the pull request, its every comment has its reasons. For example, comments can be written…&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;to-learn&quot;&gt;To Learn&lt;&#x2F;h4&gt;
&lt;p&gt;The closest to the main goal of code review “genre” of comments is “why is it written &lt;em&gt;precisely like that&lt;&#x2F;em&gt;?”, but it isn’t used often enough. Even if all participants already &lt;em&gt;know&lt;&#x2F;em&gt; the reason, but it wasn’t mentioned explicitly anywhere, the need to formulate the answer to the question will spotlight accidental excessive complexity&lt;&#x2F;p&gt;
&lt;p&gt;If they &lt;em&gt;think&lt;&#x2F;em&gt; they know the reason, then explicitly writing your point of view might highlight when that “know” was only an assumption. Assumptions lead to bugs, so sometimes it’s better to clarify even something “obvious”. If that “obvious” was already written down somewhere, it’s completely okay to get a link to a comment that has the answer or to a commit with an explanation&lt;&#x2F;p&gt;
&lt;p&gt;Sadly, this genre is also the most dangerous. After reading “why is it written &lt;em&gt;precisely like that&lt;&#x2F;em&gt;?” in a bad mood, I might wind myself up thinking “how can they doubt my work?” and&#x2F;or “seem like here, I can’t do this &lt;em&gt;precisely like that&lt;&#x2F;em&gt;, so I’ll rewrite everything ASAP”. Because of that, it’s important to let pull request’s author know that the question doesn’t have any hidden meaning&lt;&#x2F;p&gt;
&lt;p&gt;It’s really difficult to transmit the tone&#x2F;intonation in the text alone, so, to make sure the text reads closer to how it sounds in your head, you’ll need to somehow compensate that lack of tone&#x2F;intonation. For example, by rephrasing the question, emphasising with italics, just writing “I’m sorry, I’m not implying anything here”, or by adding a drop of emotions with emoji&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;sarahcandersen.com&#x2F;post&#x2F;618916011346968576&quot;&gt;
              &lt;img alt&#x3D;&quot;Sarah&#39;s
Scribbles&quot; src&#x3D;&quot;&#x2F;media&#x2F;WDgPiySzF4naFcqy5kOXwkIuLJ.jpeg&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;sarahcandersen.com&#x2F;post&#x2F;618916011346968576&quot;&gt;&lt;b&gt;Sarah&#39;s
Scribbles&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Tumblr Blog
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;h4 id&#x3D;&quot;to-correct&quot;&gt;To Correct&lt;&#x2F;h4&gt;
&lt;p&gt;A much more popular genre of pull request comments is “that’s wrong”. Many people think that these comments are &lt;em&gt;the point&lt;&#x2F;em&gt; of reviewing the code. Agreed, an additional pair of eyeballs will help discovering problems sooner, but before critiquing the changes it’s important to first understand the reasons behind them. Without that, both sides of code review will make each other’s mood worse — either with bad advice or with surface-level changes to resolve that annoying comment&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;blockquote cite&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;
        Chesterton&#39;s Fence:&lt;br&gt;&lt;br&gt;&quot;If you don&#39;t see the use of it, I certainly won&#39;t let you clear it away. Go away and think. Then, when you can come back and tell me that you do see the use of it, I may allow you to destroy it.&quot;
      &lt;&#x2F;blockquote&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;&lt;b&gt;Lars &quot;Sweet Leaf&quot; Doucet on Twitter&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;Even if you know, what was the reason behind &lt;em&gt;this&lt;&#x2F;em&gt; change, you don’t have to ask to fix every single line in which you’ve noticed a problem&lt;&#x2F;p&gt;
&lt;p&gt;Firstly, it might not be an actual problem, so asking “why?” would work better. Secondly, one remembers their own experience better than someone else’s, so if a problem isn’t critical and&#x2F;or will quickly show itself in runtime and&#x2F;or simple to hide from users, ten minutes of panic would teach code author &lt;em&gt;a lot&lt;&#x2F;em&gt;. For example, that’s how young me learned to hide new features behind &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Feature_toggle&quot;&gt;feature toggles&lt;&#x2F;a&gt; and to invalidate caches&lt;&#x2F;p&gt;
&lt;p&gt;When problem is actually worth a comment, it’s better to leave similar comments all at once, to lessen the time until the whole pull request is merged&#x2F;closed. In other words, prefer “comment-comment-fix-fix” over “comment-fix-comment-fix”&lt;&#x2F;p&gt;
&lt;p&gt;In addition to pointing at a problem, reviewer can also suggest a solution, be it just text, pseudocode, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;collaborating-with-issues-and-pull-requests&#x2F;incorporating-feedback-in-your-pull-request#applying-a-suggested-change&quot;&gt;&lt;code&gt;&#x60;&#x60;&#x60;suggestion&#x60;&#x60;&#x60;&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, or something else&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;pDBf0XDoacZCL9S2PfbvmbRTd1.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;spJeelkeTI379teRTvOQV2x1LO.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you have a good relationship with pull request’s author and you are secure about each other’s skills, you could checkout pull request’s branch and make suggestions in a form of commits. Where to push those commits — to the new branch (with a PR to a PR&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:obligatory-inception-joke&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:obligatory-inception-joke&quot; rel&#x3D;&quot;footnote&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;) or to the same branch — depends on original branch’s author. This path, just like “let them make mistakes”, is risky, but it does exist&lt;&#x2F;p&gt;
&lt;p&gt;Returning to the lack of tone in text… If a person might misread an innocent “why?”, then “I’ve written code for &lt;em&gt;your&lt;&#x2F;em&gt; pull request” or bright red “&lt;code&gt;@username&lt;&#x2F;code&gt; requested changes” will be taken personally sooner or later. So you need to intentionally soften them, or, at least, remind everyone that this is critique of the &lt;em&gt;code&lt;&#x2F;em&gt;, not of the person&lt;&#x2F;p&gt;
&lt;p&gt;But even “&lt;code&gt;@username&lt;&#x2F;code&gt; requested changes” is better than silence&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;to-format&quot;&gt;&lt;del&gt;To Format&lt;&#x2F;del&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;What you shouldn’t comment are semicolons, indents, and other formatting. If formatting is really important, then, instead of writing a “we put spaces inside the curly braces”, spend your time on setting up simple automatic formatter&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:no-config&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:no-config&quot; rel&#x3D;&quot;footnote&quot;&gt;8&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; and on running linter on CI&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2024-12-G7t5cCdFV7:hooks-arent-enough&quot; id&#x3D;&quot;rfn:post-2024-12-G7t5cCdFV7:hooks-arent-enough&quot; rel&#x3D;&quot;footnote&quot;&gt;9&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Without those, reviewer has to work to keep their focus on the pull request’s gist, instead of getting districted by surface-level and usually useless for a user things like “sort imports alphabetically”&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;to-bear-in-mind&quot;&gt;To Bear in Mind&lt;&#x2F;h4&gt;
&lt;p&gt;Reviewers’ comments don’t have to address exclusively pull request’s author, but other colleagues too&lt;&#x2F;p&gt;
&lt;p&gt;For example, to bring attention to difficulties when integrating with some other system’s API. Or to more-than-appropriate amount of manual&#x2F;boilerplate changes where refactoring&#x2F;types&#x2F;automation would simplify and shorten future pull requests. Or to similarities between the new code and code in another project (with a possibility of code share)&lt;&#x2F;p&gt;
&lt;p&gt;Immediately addressing those comments, in the same pull request, would probably over-blow its scope, but, after a few of such reminders, it’ll be easier to remember about them during a future spring planning&lt;&#x2F;p&gt;
&lt;h4 id&#x3D;&quot;to-encourage&quot;&gt;To Encourage&lt;&#x2F;h4&gt;
&lt;p&gt;If opening a pull request only ever leads to neutral and relatively negative comments, then &lt;em&gt;of course&lt;&#x2F;em&gt; people would avoid code review — “I’ve tried so hard, coded, worked around roadblocks, just to get so close to the finish line and get drowned with ‘that’s strange’, ‘that’s wrong’, ‘that’s bad’…”&lt;&#x2F;p&gt;
&lt;p&gt;To counterbalance all that, it’s useful to, at least occasionally, praise good changes, whether it’s a particularly clean solution, a good idea, or a banal “thank you, it’s been annoying me too, but I couldn’t get myself to address it”&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Without comments, code review is just a rubberstamping. Only with “that’s wrong, fix that” comments — harmful gatekeeping. I hope that this post showed that with some care, both side of code review can be a little bit nicer and useful&lt;&#x2F;p&gt;
&lt;p&gt;Thank you for your attention&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:more-of-a-suggestion&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;I want to emphasis that everything here are &lt;em&gt;opportunities&lt;&#x2F;em&gt; to make pull requests better, not &lt;em&gt;requirements&lt;&#x2F;em&gt; for them. If personal communication and a few words in title work for your PR, nobody insists on you spending hours to write an epic about a three lines change 🙏&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:more-of-a-suggestion&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:50-plus&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;When 50 symbols is not enough, you could and should extend the title with additional description (by adding a line break in &lt;code&gt;git&lt;&#x2F;code&gt; CLI or by using a dedicated text input in a GUI), whether it was written manually or generated from a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;thoughtbot.com&#x2F;blog&#x2F;better-commit-messages-with-a-gitmessage-template&quot;&gt;template&lt;&#x2F;a&gt; &lt;em&gt;(thanks to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;markbaraban&#x2F;status&#x2F;1363793616074928129&quot;&gt;Mark&lt;&#x2F;a&gt; for the advice)&lt;&#x2F;em&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:50-plus&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:vi&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;If &lt;code&gt;vi&lt;&#x2F;code&gt; is not your preferred text editor, you can &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;unix.stackexchange.com&#x2F;questions&#x2F;73484&#x2F;how-can-i-set-vi-as-my-default-editor-in-unix&quot;&gt;change &lt;code&gt;$EDITOR&lt;&#x2F;code&gt; in your &lt;code&gt;~&#x2F;.bashrc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;~&#x2F;.config&#x2F;fish&#x2F;config.fish&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to launch, for example, Sublime Text (&lt;code&gt;subl -w&lt;&#x2F;code&gt;) or any other text editor&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:vi&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:markdown-flavors&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Whether it’s &lt;a href&#x3D;&quot;http:&#x2F;&#x2F;daringfireball.net&#x2F;projects&#x2F;markdown&#x2F;&quot;&gt;Gruber’s “original”&lt;&#x2F;a&gt;, more detailed &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;guides.github.com&#x2F;features&#x2F;mastering-markdown&#x2F;&quot;&gt;GitHub Flavored Markdown&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;commonmark.org&#x2F;help&#x2F;&quot;&gt;CommonMark&lt;&#x2F;a&gt;, or another language altogether (as long as it’s the language your code review tool’s using)&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:markdown-flavors&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:tnx&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Thanks to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;wowitskatya&quot;&gt;Katya&lt;&#x2F;a&gt; for reminding about this case&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:tnx&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:6&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Performance and documentation are features too&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:6&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:obligatory-inception-joke&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;G2jUhnCU9iA&quot;&gt;BRAAAAM&lt;&#x2F;a&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:obligatory-inception-joke&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:no-config&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Preferably with minimum configurations, to not get tempted into hours long discussion of vital questions like “double quotes or single one?”. For example, check out &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;eslint&#x2F;eslint&#x2F;blob&#x2F;8984c91372e64d1e8dd2ce21b87b80977d57bff9&#x2F;conf&#x2F;eslint-recommended.js&quot;&gt;&lt;code&gt;eslint:recommended&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;prettier.io&#x2F;&quot;&gt;&lt;code&gt;prettier&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;standardjs.com&#x2F;&quot;&gt;&lt;code&gt;standard&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; in Javascript’s ecosystem, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;black.readthedocs.io&#x2F;en&#x2F;stable&#x2F;&quot;&gt;&lt;code&gt;black&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; in Python’s, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;golang.org&#x2F;cmd&#x2F;gofmt&#x2F;&quot;&gt;&lt;code&gt;gofmt&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; in Go’s, and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;editorconfig.org&#x2F;&quot;&gt;&lt;code&gt;.editorconfig&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; almost everywhere&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:no-config&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2024-12-G7t5cCdFV7:hooks-arent-enough&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;And, please, don’t let git hooks justify the lack of linter on CI. Hooks might not be ran because of one reason or another, for example if the commit was created in Github’s web UI. So, without linter on CI, your main branch will have broken formatting more often than you’d like (and you’d probably like it to be “never”)&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2024-12-G7t5cCdFV7:hooks-arent-enough&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/on-code-reviews.html</link>
      <guid isPermaLink="false">post-2024-12-G7t5cCdFV7</guid>
      <pubDate>Sun, 08 Dec 2024 17:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Server-side Rendering: CDN and Caching</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;VX1Bvz50Eh9RH9nWu5ZcyoIUAQ.png&quot; alt&#x3D;&quot;&quot; width&#x3D;&quot;800&quot; height&#x3D;&quot;400&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;aka “The Other Servers”&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Previously, I wrote about &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ssr-backstory.html&quot;&gt;why&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&quot;&gt;Nebula&lt;&#x2F;a&gt; decided to implement server-side rendering (SSR) for its web app and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ssr-the-server.html&quot;&gt;what&lt;&#x2F;a&gt; it took to do so in the web app’s codebase. And now it&#39;s time to explain the last part of delivering server-side rendered pages to the browser — Content Delivery Network (CDN)&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;cdn&quot;&gt;CDN&lt;&#x2F;h2&gt;
&lt;p&gt;CDN is a &lt;em&gt;network&lt;&#x2F;em&gt; of proxy servers (also known as “edge” servers) all over the globe. These servers can:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;route users’ requests to different servers (for example, route dynamic &lt;code&gt;&#x2F;jetlag&lt;&#x2F;code&gt; to our SSR server and static &lt;code&gt;&#x2F;index.2c3.js&lt;&#x2F;code&gt; to S3 storage)&lt;&#x2F;li&gt;
&lt;li&gt;modify the HTTP headers that server receives from and sends to a browser&lt;&#x2F;li&gt;
&lt;li&gt;run “serverless” functions (like AWS Lambdas)&lt;&#x2F;li&gt;
&lt;li&gt;cache server responses&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And, because they are all over the globe, your request for a video page doesn’t have to go under the ocean if there’s an edge server close to you and somebody else close to the same server has &lt;em&gt;just&lt;&#x2F;em&gt; loaded the same page&lt;&#x2F;p&gt;
&lt;p&gt;CDN has a huge impact on how optimized the server has to be — every request cached and served by CDN saves us a lot of server time. The Internet is &lt;em&gt;crawling&lt;&#x2F;em&gt; with bots, both benign and nefarious, so even if you aren’t operating at huge scale, at least &lt;em&gt;some&lt;&#x2F;em&gt; server caching (be it CDN or just a properly configured nginx&#x2F;Caddy&#x2F;whatever) will help dealing with those critters&lt;&#x2F;p&gt;
&lt;p&gt;There are several big companies providing CDN services, but nebula.tv uses Amazon’s CloudFront&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:and-others&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:and-others&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. I will try to keep this post generic enough for developers outside of Amazon’s influence, but AWS specifics will pop up here and there&lt;&#x2F;p&gt;
&lt;p&gt;And by “here” I mean “&lt;em&gt;here&lt;&#x2F;em&gt; is some CloudFront’s terminology”:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Distribution — a version of edge server configuration, used to route, cache, and other stuff for a specific domain&lt;&#x2F;li&gt;
&lt;li&gt;Viewer request — original HTTP request coming from a browser or other client (native app, &lt;code&gt;curl&lt;&#x2F;code&gt;, etc.)&lt;&#x2F;li&gt;
&lt;li&gt;Origin request — HTTP request sent from an edge server to wherever the distribution routes (aka “to origin”)&lt;&#x2F;li&gt;
&lt;li&gt;Origin response — HTTP response from origin to an edge server&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;FVtDXD7ApNBEtHu4WEVEuy3cnV.png&quot;&gt;
              &lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;FVtDXD7ApNBEtHu4WEVEuy3cnV.png&quot; width&#x3D;&quot;1920&quot; height&#x3D;&quot;1080&quot;&gt;
          &lt;&#x2F;a&gt;


        &lt;figcaption&gt;
            &lt;i&gt;
              Scheme of user request with CDN in the middle&lt;br&gt;1. Viewer request&lt;br&gt;2. Origin request&lt;br&gt;3. Origin response
            &lt;&#x2F;i&gt;
        &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;h2 id&#x3D;&quot;caching&quot;&gt;Caching&lt;&#x2F;h2&gt;
&lt;p&gt;So, the main point of adding a CDN layer to your architecture is caching, but it isn’t limited to CDNs — browsers also cache&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:cache-as-well&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:cache-as-well&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. In both cases, the caching is controlled by  &lt;code&gt;Cache-Control&lt;&#x2F;code&gt;  and &lt;code&gt;Vary&lt;&#x2F;code&gt; origin response headers&lt;&#x2F;p&gt;
&lt;p&gt;At its most basic, &lt;code&gt;Cache-Control&lt;&#x2F;code&gt; is an origin response header with a list of comma-separated directives:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max-age&#x3D;{number}&lt;&#x2F;code&gt; indicates to both browser and CDN to cache the response to this URL and re-use it if there are viewer requests in the next &lt;code&gt;{number}&lt;&#x2F;code&gt; seconds&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;s-maxage&#x3D;{number}&lt;&#x2F;code&gt; is similar, but for CDNs only — browsers just ignore it&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;public&lt;&#x2F;code&gt; indicates that response isn’t specific to a user&#x2F;session, so can be stored in a shared cache&lt;&#x2F;li&gt;
&lt;li&gt;there’s also &lt;code&gt;no-cache&lt;&#x2F;code&gt;, &lt;code&gt;no-store&lt;&#x2F;code&gt;, &lt;code&gt;private&lt;&#x2F;code&gt;, &lt;code&gt;immutable&lt;&#x2F;code&gt;, and others. The post is already getting long, so just read about those &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTTP&#x2F;Headers&#x2F;Cache-Control&quot;&gt;on MDN&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Why would you want cache on CDN and not in a browser? Mostly&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:less-mostly&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:less-mostly&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, to recover from errors — if server responded with broken code or data, and browser cached it for an hour, for that hour user will see the broken site&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:full-reload&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:full-reload&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. On the other hand, if CDN has previously-broken-but-now-fixed response in its cache, there’s usually a button or an API to invalidate caches on edge servers, forcing them to load the latest version of whatever&lt;&#x2F;p&gt;
&lt;p&gt;While &lt;code&gt;Cache-Control&lt;&#x2F;code&gt; tells browser&#x2F;CDN whether or not to cache a particular response, &lt;code&gt;Vary&lt;&#x2F;code&gt; tells what in viewer request will invalidate that cache. You see, by default, the browser’s “cache key” (identifier for data in cache) is the viewer request’s HTTP method and URL. But if a server can respond &lt;em&gt;differently&lt;&#x2F;em&gt; to requests with the same URL depending on viewer request headers (for example, user-specific data if there’s a session token in &lt;code&gt;Cookie&lt;&#x2F;code&gt;), these headers can be listed in &lt;code&gt;Vary&lt;&#x2F;code&gt;. Support for &lt;code&gt;Vary&lt;&#x2F;code&gt; header on CDNs is spotty — for example, Cloud&lt;i&gt;Front&lt;&#x2F;i&gt; does understand it (albeit with some &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;RequestAndResponseBehaviorCustomOrigin.html#ResponseCustomContentNegotiation&quot;&gt;gotchas&lt;&#x2F;a&gt;), Cloud&lt;i&gt;Flare&lt;&#x2F;i&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;cache&#x2F;about&#x2F;cache-control&#x2F;#other&quot;&gt;basically ignores&lt;&#x2F;a&gt; it&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:cloudffff&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:cloudffff&quot; rel&#x3D;&quot;footnote&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;aws-specifics-policies&quot;&gt;AWS Specifics: Policies&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;if you aren’t interested in CloudFront minutia, you can skip to the &lt;a href&#x3D;&quot;#what-and-how&quot;&gt;next section&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To better control the “cache key”, CloudFront has two “policies” that can finely tune distribution’s default and custom endpoints’ behavior&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;controlling-the-cache-key.html&quot;&gt;Cache policy&lt;&#x2F;a&gt; describes what’s included in the “cache key” for each particular viewer request. By default, it&#39;s only URL pathname, but we can add all&#x2F;allowlisted&#x2F;blocklisted query params, headers, or cookies. For example, we can ignore &lt;code&gt;fbclid&lt;&#x2F;code&gt; and &lt;code&gt;ref&lt;&#x2F;code&gt; query param, and&#x2F;or include only &lt;code&gt;session&lt;&#x2F;code&gt; cookie. That way these requests will have the same “cache key”:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-plain&quot;&gt;https:&#x2F;&#x2F;nebula.tv&#x2F;search?q&#x3D;coconut
Cookie: session&#x3D;patrick

https:&#x2F;&#x2F;nebula.tv&#x2F;search?q&#x3D;coconut
Cookie: session&#x3D;patrick; analytics&#x3D;dave-the-agent

https:&#x2F;&#x2F;nebula.tv&#x2F;search?q&#x3D;coconut&amp;amp;ref&#x3D;gmtk
Cookie: session&#x3D;patrick
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;controlling-origin-requests.html&quot;&gt;Origin request policy&lt;&#x2F;a&gt; describes what query params&#x2F;headers&#x2F;cookies have to be added to the request from CDN to SSR when the request&#39;s &quot;cache key&quot; is &quot;missed&quot; (&#x3D; &quot;not in CDN cache&quot;). These query params&#x2F;headers&#x2F;cookies don&#39;t affect the “cache key&quot;, leading to possible issues — for example, if the cache policy ignores &lt;code&gt;User-Agent&lt;&#x2F;code&gt; header&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:ignore-user-agent&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:ignore-user-agent&quot; rel&#x3D;&quot;footnote&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, but the origin request policy includes it, Firefox users might get cached responses for Chrome, if Chrome was the first to hit a particular endpoint first. So use origin request policies very carefully&lt;&#x2F;p&gt;
&lt;p&gt;Everything outside of these policies won’t reach your server&#x2F;origin&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;4t3KaaVRYUirs0Vbis04qVnhmb.png&quot;&gt;
              &lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;4t3KaaVRYUirs0Vbis04qVnhmb.png&quot; width&#x3D;&quot;1920&quot; height&#x3D;&quot;1080&quot;&gt;
          &lt;&#x2F;a&gt;


        &lt;figcaption&gt;
            &lt;i&gt;
              Example of how cache and origin request policies limit what data from viewer request goes into cache key and what reaches the origin
            &lt;&#x2F;i&gt;
        &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;AWS has a list of predefined (“managed”) &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;using-managed-cache-policies.html&quot;&gt;cache&lt;&#x2F;a&gt; and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;using-managed-origin-request-policies.html&quot;&gt;origin request&lt;&#x2F;a&gt; policies, which one can use by simply modifying the CloudFront distribution. If those aren’t enough for your needs, you can create your own&lt;&#x2F;p&gt;
&lt;p&gt;If you do create your own policy, avoid allowlisting query params. We learned the hard way that ignoring everything but a predefined list of params leads to nasty bugs if &lt;em&gt;somebody&lt;&#x2F;em&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:anti-hero&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:anti-hero&quot; rel&#x3D;&quot;footnote&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; forgets to update that list&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;what-and-how&quot;&gt;What and How&lt;&#x2F;h2&gt;
&lt;p&gt;What and how to cache depends &lt;em&gt;a lot&lt;&#x2F;em&gt; on your web app’s needs and what types of responses it serves. For nebula.tv, the needs are:&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;public-assets&quot;&gt;Public assets&lt;&#x2F;h3&gt;
&lt;p&gt;In other words, version-controlled and uploaded as-is files, for example &lt;code&gt;favicon.ico&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We don’t cache those. Maybe, we should cache them on edge servers, but don’t — these files don’t block load of the web app, so S3 is fast enough&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;built-assets-with-hash-in-the-filename&quot;&gt;Built assets with hash in the filename&lt;&#x2F;h3&gt;
&lt;p&gt;After a built tool (like &lt;code&gt;vite&lt;&#x2F;code&gt;) has done its job, output files usually have the file’s hash in their name (for example, &lt;code&gt;2c3&lt;&#x2F;code&gt; in  &lt;code&gt;index.2c3.js&lt;&#x2F;code&gt;). Once the file is uploaded it’ll never change&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:no-collisions&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:no-collisions&quot; rel&#x3D;&quot;footnote&quot;&gt;8&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, so both CDN and browser can cache it for a long time&lt;&#x2F;p&gt;
&lt;p&gt;So, when &lt;em&gt;adding&lt;&#x2F;em&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:add-not-replace&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:add-not-replace&quot; rel&#x3D;&quot;footnote&quot;&gt;9&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; files like this to S3, we use &lt;code&gt;--cache-control max-age&#x3D;86400,public&lt;&#x2F;code&gt; option of &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;cli&#x2F;latest&#x2F;reference&#x2F;s3&#x2F;cp.html&quot;&gt;S3 cli&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;html-responses-with-hardcoded-metadata&quot;&gt;HTML responses with hardcoded metadata&lt;&#x2F;h3&gt;
&lt;p&gt;These are HTML pages returned from SSR server that don’t change without a code deploy. In the case of Nebula, that’s pages like &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&#x2F;classes&quot;&gt;Classes&lt;&#x2F;a&gt; or &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&#x2F;videos&quot;&gt;Videos&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We don’t want to cache those in a browser, CDN can cache them for long&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:only-a-day&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:only-a-day&quot; rel&#x3D;&quot;footnote&quot;&gt;10&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, and these pages don’t have data specific to a user&lt;&#x2F;p&gt;
&lt;p&gt;So, easy-peasy! When sending a response &lt;code&gt;renderedHtml&lt;&#x2F;code&gt; in SSR server, we’ll add &lt;code&gt;Cache-Control&lt;&#x2F;code&gt; with &lt;code&gt;public&lt;&#x2F;code&gt; and &lt;code&gt;s-maxage&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeHead&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;200&lt;&#x2F;span&gt;, {
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;Content-Type&#39;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;text&#x2F;html&#39;&lt;&#x2F;span&gt;,
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;Cache-Control&#39;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;public, s-maxage&#x3D;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${MAXAGE_IN_SECONDS}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;,
});

res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;end&lt;&#x2F;span&gt;(renderedHtml);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id&#x3D;&quot;content-specific-metadata-html-responses&quot;&gt;Content-specific metadata HTML responses&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Crap, I shouldn’t had used a constant in &lt;code&gt;res.writeHead&lt;&#x2F;code&gt;…&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;These are pages like a video page — it has, for example, the video’s thumbnail in &lt;code&gt;&amp;lt;meta property&#x3D;&quot;og:url&quot;&amp;gt;&lt;&#x2F;code&gt; and video creator can change that thumbnail whenever. To account for that, these pages are cached for five minutes. Yeah, it isn’t instantaneous, but it also doesn’t complicate our content management system (CMS) with “cache invalidation” API calls to CloudFront&lt;&#x2F;p&gt;
&lt;p&gt;To change the default &lt;code&gt;MAXAGE_IN_SECONDS&lt;&#x2F;code&gt; for these pages, we’re using the &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;v5.reactrouter.com&#x2F;web&#x2F;api&#x2F;StaticRouter&#x2F;context-object&quot;&gt;&lt;code&gt;context&lt;&#x2F;code&gt; property&lt;&#x2F;a&gt; of React Router’s &lt;code&gt;&amp;lt;StaticRouter&amp;gt;&lt;&#x2F;code&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:react-router-5&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:react-router-5&quot; rel&#x3D;&quot;footnote&quot;&gt;11&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. This property is an object that gets automatically passed to route components as &lt;code&gt;staticContext&lt;&#x2F;code&gt; and can be modified in those components. So, whenever a page includes CMS-defined metadata, we’re passing a smaller value for &lt;code&gt;s-maxage&lt;&#x2F;code&gt; via that object:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-jsx&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; server.jsx&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; routerContext &#x3D; { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;cacheDuration&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;undefined&lt;&#x2F;span&gt; };

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; renderedHtml &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ReactDOMServer&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;renderToString&lt;&#x2F;span&gt;(
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;language-xml&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;StaticRouter&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;location&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;{url}&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;context&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;{routerContext}&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;App&lt;&#x2F;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;StaticRouter&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
);

res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeHead&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;200&lt;&#x2F;span&gt;, {
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;Content-Type&#39;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;text&#x2F;html&#39;&lt;&#x2F;span&gt;,
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;Cache-Control&#39;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;public, s-maxage&#x3D;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${routerContext.cacheDuration ?? MAXAGE_IN_SECONDS}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;,
});

res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;end&lt;&#x2F;span&gt;(renderedHtml);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-jsx&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; pages&#x2F;Video.jsx&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;STATIC_CACHE_DURATION&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;300&lt;&#x2F;span&gt;; &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; 5 minutes&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;export&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;Video&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;{ staticContext }&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (staticContext) {
    staticContext.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;cacheDuration&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;STATIC_CACHE_DURATION&lt;&#x2F;span&gt;;
  }

  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;error-responses&quot;&gt;Error responses&lt;&#x2F;h2&gt;
&lt;p&gt;But that’s when everything goes smoothly. But it &lt;del&gt;never&lt;&#x2F;del&gt; not always does&lt;&#x2F;p&gt;
&lt;p&gt;If something goes “wrong”, for example user loads a video page a minute before it’s published, we don’t want to serve cached 404 error for the next four minutes. So treat responses with 4XX or 5XX HTTP codes differently — either don’t cache them entirely, cache them for a shorter time, or something&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;aws-specifics-failover&quot;&gt;AWS Specifics: Failover&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;em&gt;Once again, &lt;a href&#x3D;&quot;#how-it-went&quot;&gt;click here&lt;&#x2F;a&gt; to skip&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;CloudFront has a notion of &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonCloudFront&#x2F;latest&#x2F;DeveloperGuide&#x2F;high_availability_origin_failover.html&quot;&gt;failover&lt;&#x2F;a&gt;, for when the origin can’t be reached or if it returns some error code. SPAs frequently use fallbacks for a client-side routing: CloudFront receives a request for &lt;code&gt;&#x2F;something&lt;&#x2F;code&gt;, goes to SSR, gets a &lt;code&gt;HTTP 500&lt;&#x2F;code&gt; from it, and falls back to serving &lt;code&gt;&#x2F;index.html&lt;&#x2F;code&gt;, which is (hopefully) present on S3&lt;&#x2F;p&gt;
&lt;p&gt;nebula.tv uses fallbacks to hide 5XX errors from users by doing basically the same — if SSR can’t be reached (&lt;code&gt;HTTP 502&lt;&#x2F;code&gt;) or can’t process the request due to code error (&lt;code&gt;HTTP 500&lt;&#x2F;code&gt;), it’ll just serve &lt;code&gt;index.html&lt;&#x2F;code&gt; from S3. That way, SSR errors only affect bots while humans visitors will have the same experience as before&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:unless-devs&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:unless-devs&quot; rel&#x3D;&quot;footnote&quot;&gt;12&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; cloudfront-distribution.json&lt;&#x2F;span&gt;

{
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;CustomErrorResponses&quot;&lt;&#x2F;span&gt;: {
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Quantity&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;2&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; oh yeah, CloudFront can&#39;t count &#x60;Items&#x60; by itself&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Items&quot;&lt;&#x2F;span&gt;: [
      {
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ErrorCode&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;500&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ResponsePagePath&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&#x2F;index.html&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ResponseCode&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;500&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ErrorCachingMinTTL&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;
      },
      {
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ErrorCode&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;502&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ResponsePagePath&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&#x2F;index.html&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ResponseCode&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;502&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ErrorCachingMinTTL&quot;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;
      }
    ]
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While this is quite handy, it does create problems with JSON endpoints on the same distribution — if such endpoint returns &lt;code&gt;HTTP 500&lt;&#x2F;code&gt; with some data in JSON response, CloudFront will &lt;em&gt;helpfully&lt;&#x2F;em&gt; replace that response with &lt;code&gt;index.html&lt;&#x2F;code&gt;. Not sure how to solve that, but maybe something will come up…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;how-it-went&quot;&gt;How it went&lt;&#x2F;h2&gt;
&lt;p&gt;After all that work was done, I &lt;em&gt;think&lt;&#x2F;em&gt; it was worth it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;requests with &lt;em&gt;warm&lt;&#x2F;em&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:hot-and-cold&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:hot-and-cold&quot; rel&#x3D;&quot;footnote&quot;&gt;13&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; CDN cache sped up from 1-1.5 seconds to 20-50ms&lt;&#x2F;li&gt;
&lt;li&gt;Google discovered that Nebula has &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.google.com&#x2F;search?q&#x3D;site:nebula.tv&amp;amp;tbm&#x3D;vid&amp;amp;tbs&#x3D;qdr:w&quot;&gt;&lt;em&gt;videos&lt;&#x2F;em&gt;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;more messengers started to show nice link previews&lt;&#x2F;li&gt;
&lt;li&gt;the server component specifically for the web app is great to have in the tool belt&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2023-05-ocwaYcPOIF:feature-toggles&quot; id&#x3D;&quot;rfn:post-2023-05-ocwaYcPOIF:feature-toggles&quot; rel&#x3D;&quot;footnote&quot;&gt;14&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Although, requests with &lt;em&gt;cold&lt;&#x2F;em&gt; CDN caches  — loading an unpopular page can take &lt;em&gt;seconds&lt;&#x2F;em&gt; to finish. At some point, I hope we’ll skip more API requests on the server and&#x2F;or share the React Query cache between SSR processes, but not today&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;thank-you&quot;&gt;Thank you&lt;&#x2F;h2&gt;
&lt;p&gt;Thank you for your attention. If you liked these posts, please consider &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;stand-with-ukraine.pp.ua&quot;&gt;supporting Ukraine&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Thanks to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;samwho.dev&quot;&gt;Sam Rose&lt;&#x2F;a&gt; for the help with writing this. Photo by &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;@taylorvanriper925&quot;&gt;Taylor Van Riper&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:and-others&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Also Fastly and, since recently, CloudFlare, but that’s not important for this post&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:and-others&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:cache-as-well&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;As well as servers, databases, processors, and almost everything else computer’y, but don’t worry about that now&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:cache-as-well&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:less-mostly&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;And, &lt;em&gt;less mostly&lt;&#x2F;em&gt;, to better control when new features become accessible&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:less-mostly&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:full-reload&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;If you’re looking for a comment section to type out “But they can reload with cache disabled!”, don’t bother&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:full-reload&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:cloudffff&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Do I &lt;em&gt;hate&lt;&#x2F;em&gt; that two CDNs we’re using share the “CF” abbreviation!&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:cloudffff&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:ignore-user-agent&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Which is a good idea, since this header rarely affects the HTTP response but has &lt;em&gt;so&lt;&#x2F;em&gt; many variants that cache will rarely be warm&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:ignore-user-agent&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:anti-hero&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;cjJ9MKvKTwG3hLmtp6maJJ4ugd&#x2F;gifv.mp4&quot; title&#x3D;&quot;It’s me. Hi. I’m the problem, it’s me&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot;&gt;&lt;&#x2F;video&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:anti-hero&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:no-collisions&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Assuming there are no hash collisions&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:no-collisions&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:add-not-replace&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Because we want to have older assets available to avoid 404s if browser has stale cache or if we did a version rollback&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:add-not-replace&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:only-a-day&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Although, nebula.tv caches them for a day, which is long&lt;i&gt;ish&lt;&#x2F;i&gt;, but not that long. Can’t remember why… Maybe, just to be safe?..&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:only-a-day&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:react-router-5&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;We’re still on &lt;code&gt;react-router@5&lt;&#x2F;code&gt; because, until recently, the sixth version had a few issues that made it harder to migrate without a performance hit&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:react-router-5&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:unless-devs&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Unless they are developers who browse with an open Web Inspector 😅&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:unless-devs&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:hot-and-cold&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Cache is warm when it has data to respond without reaching to the origin, and cold when it doesn’t and needs to wait on the origin to respond&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:hot-and-cold&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2023-05-ocwaYcPOIF:feature-toggles&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;For example, to have a better experience for &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Feature_toggle&quot;&gt;feature toggles&lt;&#x2F;a&gt; — client-side-only web app has to wait on fresh flags to load. Or risk the UI flashing&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2023-05-ocwaYcPOIF:feature-toggles&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/ssr-cdn.html</link>
      <guid isPermaLink="false">post-2023-05-ocwaYcPOIF</guid>
      <pubDate>Wed, 17 May 2023 17:40:00 GMT</pubDate>
    </item>
    <item>
      <title>Server-side Rendering: The Server</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;7Pm4z3nXonrncaV5z8H5gwIn4m.png&quot; alt&#x3D;&quot;&quot; width&#x3D;&quot;800&quot; height&#x3D;&quot;400&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Backstory was boring. How about some code this time?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ssr-backstory.html&quot;&gt;the previous part&lt;&#x2F;a&gt; I described how &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&quot;&gt;Nebula&lt;&#x2F;a&gt; arrived at the decision to implement server-side rendering (SSR) for the web app. Then came the hard part: actually doing&amp;nbsp;it&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;goal&quot;&gt;Goal&lt;&#x2F;h2&gt;
&lt;p&gt;Route requests to our server, render the React app as static HTML, and serve &lt;em&gt;that&lt;&#x2F;em&gt; &lt;&#x2F;p&gt;
&lt;p&gt;To achieve this goal, the server needs to do just three things:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Determine what page is requested&lt;&#x2F;li&gt;
&lt;li&gt;Query HTTP APIs for data to render&lt;&#x2F;li&gt;
&lt;li&gt;Render HTML&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Because the web app is already built on React, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;reactrouter.com&#x2F;&quot;&gt;&lt;code&gt;react-router&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;tanstack.com&#x2F;query&#x2F;v4&quot;&gt;&lt;code&gt;react-query&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, all three were taken care of:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;react-router&lt;&#x2F;code&gt; picks a page component depending on &lt;code&gt;location.href&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;react-query&lt;&#x2F;code&gt; loads data&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ReactDOMServer.renderToString&lt;&#x2F;code&gt; renders everything to HTML&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Wrap everything into an &lt;code&gt;express&lt;&#x2F;code&gt; server, bundle that into a Docker container — and we’re done, easy-peasy&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;vunAYxUsKJGqiT20RnK5nDBN3b.jpeg&quot; alt&#x3D;&quot;NOPE&quot; width&#x3D;&quot;1280&quot; height&#x3D;&quot;610&quot;&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;browser-apis&quot;&gt;Browser APIs&lt;&#x2F;h2&gt;
&lt;p&gt;There’s no &lt;code&gt;location.href&lt;&#x2F;code&gt; in Node, just like there’s no &lt;code&gt;window&lt;&#x2F;code&gt;, &lt;code&gt;document&lt;&#x2F;code&gt;, nor &lt;code&gt;navigator&lt;&#x2F;code&gt;. In the case of &lt;code&gt;react-router&lt;&#x2F;code&gt;, it’s not much of a problem, because Node’s &lt;code&gt;request.url&lt;&#x2F;code&gt; does basically the same job. But what about things like &lt;code&gt;window.addEventListener&lt;&#x2F;code&gt;?&lt;&#x2F;p&gt;
&lt;p&gt;Usually, a React app would access these APIs only in &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;reactjs.org&#x2F;docs&#x2F;hooks-intro.html&quot;&gt;hooks&lt;&#x2F;a&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:wtf-hooks&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:wtf-hooks&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, which partially solves the problem — one of the basic hooks, &lt;code&gt;useEffect&lt;&#x2F;code&gt;, isn’t called when a page is rendered to a string (e.g. “on a server”). So it&#39;s safe to call Browser APIs there&lt;&#x2F;p&gt;
&lt;p&gt;There are still other common patterns of accessing Browser APIs, like initial values for the &lt;code&gt;useState&lt;&#x2F;code&gt; hook or defining some global constant&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:global-bad&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:global-bad&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; that won’t change after page load. For these, one has to either mock browser APIs with tools like &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;jsdom&#x2F;jsdom&quot;&gt;&lt;code&gt;jsdom&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; or check for globals to be defined. The former is rather heavy to do for SSR, so we opted to using &lt;code&gt;window?.&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;typeof window &#x3D;&#x3D;&#x3D; &#39;undefined&#39;&lt;&#x2F;code&gt;, wrapped into a self-explanatory &lt;code&gt;isSSR&lt;&#x2F;code&gt; function&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;checks&quot;&gt;Checks&lt;&#x2F;h3&gt;
&lt;p&gt;The Nebula web app is written in TypeScript (TS) and it would be nice to express “&lt;code&gt;window&lt;&#x2F;code&gt; might be undefined, but it’s &lt;em&gt;always&lt;&#x2F;em&gt; defined in &lt;code&gt;useEffect&lt;&#x2F;code&gt; body” with its type system. As far as I know, it’s not possible at the moment, so we can’t rely on TS to catch all uses of the Browser API without taking a hit in development experience (“why do I need to check for &lt;code&gt;window&lt;&#x2F;code&gt; in effects?! boo, typescript is bad!”)&lt;&#x2F;p&gt;
&lt;p&gt;So we’re checking it with good ol’ smoke tests. Loop over a list of the web app’s URLs, request HTML, check if the response status code is either 200 or 404, and throw a stack trace if the status code is 500. Simple. Two things that complicate things:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Some URLs depend on the environment, e.g. a video can be in production but not in staging, and vice versa. Because of these pages, “list of web app’s URLs“ is actually “list of &lt;code&gt;async&lt;&#x2F;code&gt; functions that return URLs” — if a page has consistent URL across environments, it would just return a string, otherwise it can access API to get &lt;em&gt;some&lt;&#x2F;em&gt; video and return its permalink&lt;&#x2F;li&gt;
&lt;li&gt;SSR has to return correct HTTP codes. More on that later&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id&#x3D;&quot;queries-and-cache&quot;&gt;Queries and Cache&lt;&#x2F;h2&gt;
&lt;p&gt;Okay, next; loading data&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;react-query&lt;&#x2F;code&gt; is a wonderful collection of hooks that greatly simplify working with HTTP APIs&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:redux&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:redux&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. But, since &lt;code&gt;useEffect&lt;&#x2F;code&gt; isn’t called on a server, SSR has to do querying itself. Thankfully, &lt;code&gt;react-query&lt;&#x2F;code&gt; provides methods to do just that&lt;&#x2F;p&gt;
&lt;p&gt;When using &lt;code&gt;react-query&lt;&#x2F;code&gt;, the app has a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;tanstack.com&#x2F;query&#x2F;v4&#x2F;docs&#x2F;reference&#x2F;QueryClientProvider&quot;&gt;&lt;code&gt;QueryClientProvider&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; somewhere in the React virtual DOM with a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;tanstack.com&#x2F;query&#x2F;v4&#x2F;docs&#x2F;reference&#x2F;QueryClient&quot;&gt;&lt;code&gt;QueryClient&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; object. This object keeps track of all the API queries created by the child components and holds their state and data. To run those queries in the absence of &lt;code&gt;useEffect&lt;&#x2F;code&gt;, the server has to:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Render the page &lt;em&gt;without&lt;&#x2F;em&gt; API data&lt;&#x2F;li&gt;
&lt;li&gt;Fetch queries&lt;&#x2F;li&gt;
&lt;li&gt;Render the page &lt;em&gt;with&lt;&#x2F;em&gt; API data&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The first step is straightforward&lt;&#x2F;p&gt;
&lt;p&gt;Running queries is a bit more tricky because they can be disabled (or already loaded&#x2F;failed, more on that later), but a pair of &lt;code&gt;filter&lt;&#x2F;code&gt;s does the job — one for &lt;code&gt;enabled&lt;&#x2F;code&gt;, another for &lt;code&gt;idle&lt;&#x2F;code&gt; status (&#x3D; “needs to be fetched”)&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:js-not-ts&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:js-not-ts&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getIdleQueries&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;queryClient&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; queryCache &#x3D; queryClient.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getQueryCache&lt;&#x2F;span&gt;();
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; queries &#x3D; queryCache.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;findAll&lt;&#x2F;span&gt;();

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; (
    queries
      .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;filter&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;q&lt;&#x2F;span&gt;) &#x3D;&amp;gt;&lt;&#x2F;span&gt; q.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;options&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;enabled&lt;&#x2F;span&gt; !&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;false&lt;&#x2F;span&gt;)
      .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;filter&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;q&lt;&#x2F;span&gt;) &#x3D;&amp;gt;&lt;&#x2F;span&gt; q.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;state&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;status&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;idle&#39;&lt;&#x2F;span&gt;)
  );
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After we’ve got the queries to run, we create a &lt;code&gt;Promise&lt;&#x2F;code&gt; for each of them and wait for them to settled (i.e. be either resolved or rejected):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;async&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;extractAndFetchQueries&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;queryClient&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; queries &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getIdleQueries&lt;&#x2F;span&gt;(queryClient);
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; fetchPromises &#x3D; queries.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;map&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;async&lt;&#x2F;span&gt; (q) &#x3D;&amp;gt; {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { onSuccess } &#x3D; q.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;options&lt;&#x2F;span&gt; || {};
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; queryResponse &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; queryClient.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;fetchQuery&lt;&#x2F;span&gt;(q.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;queryKey&lt;&#x2F;span&gt;, q.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;options&lt;&#x2F;span&gt;);
    onSuccess?.(queryResponse);
  });

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; promiseResults &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Promise&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;allSettled&lt;&#x2F;span&gt;(fetchPromises);
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now &lt;code&gt;queryClient&lt;&#x2F;code&gt; has data for initial render, but some promises might have been rejected. That’s why we’ve saved them in &lt;code&gt;values&lt;&#x2F;code&gt;. If there’s a failed query, we need to get the error for future use on the CDN and clean up them from cache:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;async&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;extractAndFetchQueries&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;queryClient&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; ...&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; queryError &#x3D; promiseResults
    .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;find&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;{ status }&lt;&#x2F;span&gt;) &#x3D;&amp;gt;&lt;&#x2F;span&gt; status &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;rejected&#39;&lt;&#x2F;span&gt;)?.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;reason&lt;&#x2F;span&gt;;
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (queryError) {
    queryClient.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;removeQueries&lt;&#x2F;span&gt;({
      &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;predicate&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;query&lt;&#x2F;span&gt;) { &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; query.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;state&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;status&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;error&#39;&lt;&#x2F;span&gt; },
    });
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;throw&lt;&#x2F;span&gt; queryError;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After all that, the server does what it needs to do, resulting in this humble virtual DOM root:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-jsx&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; renderedHtml &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ReactDOMServer&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;renderToString&lt;&#x2F;span&gt;(
  &lt;span class&#x3D;&quot;language-xml&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;QueryClientProvider&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;client&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;{queryClient}&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;StaticRouter&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;location&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;{url}&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;App&lt;&#x2F;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;StaticRouter&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;QueryClientProvider&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id&#x3D;&quot;are-we-done&quot;&gt;Are we done?&lt;&#x2F;h3&gt;
&lt;p&gt;In theory, now we have our web app rendered with API data — we just insert it into &lt;code&gt;&amp;lt;div id&#x3D;&quot;root&quot;&amp;gt;&amp;lt;&#x2F;div&amp;gt;&lt;&#x2F;code&gt; in a barebones &lt;code&gt;index.html&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In practice, Nebula does steps “2. Fetch queries” and “3. Render page &lt;em&gt;with&lt;&#x2F;em&gt; API data” two more times to make sure there are no more &lt;code&gt;idle&lt;&#x2F;code&gt; queries. For example, when loading &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&#x2F;jetlag?tab&#x3D;playlists&quot;&gt;&lt;code&gt;&#x2F;jetlag?tab&#x3D;playlists&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, we first ask API for the channel and, if it was found, for playlists associated with it. But even if the server doesn’t do enough repeats to load &lt;em&gt;everything&lt;&#x2F;em&gt;, it’s okay — the static HTML will include placeholders or default values for a browser to overwrite&lt;&#x2F;p&gt;
&lt;p&gt;Additionally, &lt;code&gt;queryClient&lt;&#x2F;code&gt;’s cache is kept for a minute in the server’s memory to skip API requests for frequently needed data, like the list of video&#x2F;channel categories&lt;&#x2F;p&gt;
&lt;p&gt;Plus, the server response is not just HTML, but also…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;http-codes&quot;&gt;HTTP codes&lt;&#x2F;h2&gt;
&lt;p&gt;What if there’s an error from the API? The one that we found in &lt;code&gt;promiseResults&lt;&#x2F;code&gt; and &lt;code&gt;throw queryError&lt;&#x2F;code&gt;?&lt;&#x2F;p&gt;
&lt;p&gt;In that case the server should respond with a non-&lt;code&gt;200&lt;&#x2F;code&gt; HTTP code and render whatever HTML is ready. Doing this is important for the CDN layer, web crawlers (to let them know that crawled URL isn’t available), and for our smoke tests mentioned before&lt;&#x2F;p&gt;
&lt;p&gt;For the Nebula web app, if the &lt;code&gt;queryError&lt;&#x2F;code&gt; is an &lt;code&gt;AxiosError&lt;&#x2F;code&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:wtf-is-axios&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:wtf-is-axios&quot; rel&#x3D;&quot;footnote&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; with 4XX status, we just pretend that the page is not found:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;try&lt;&#x2F;span&gt; {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;extractAndFetchQueries&lt;&#x2F;span&gt;(queryClient);
} &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;catch&lt;&#x2F;span&gt; (e) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (
    axios.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;isAxiosError&lt;&#x2F;span&gt;(e) &amp;amp;&amp;amp;
    e.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;response&lt;&#x2F;span&gt; &amp;amp;&amp;amp;
    e.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;response&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;status&lt;&#x2F;span&gt; &amp;gt;&#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;400&lt;&#x2F;span&gt; &amp;amp;&amp;amp;
    e.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;response&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;status&lt;&#x2F;span&gt; &amp;lt; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;500&lt;&#x2F;span&gt;
  ) {
    res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeHead&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;404&lt;&#x2F;span&gt;, { &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;Content-Type&#39;&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;text&#x2F;html&#39;&lt;&#x2F;span&gt; });
    res.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;end&lt;&#x2F;span&gt;(html);
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt;;
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;throw&lt;&#x2F;span&gt; e;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If the API responded with 5XX or if the SSR server failed to render the page, we just return a 500 status code with &lt;code&gt;Internal Server Error&lt;&#x2F;code&gt; as the body (and a stack trace when not in the production environment). But neither human nor bot visitors will see these three words because we’ll handle that on the CDN&lt;&#x2F;p&gt;
&lt;p&gt;Other common HTTP codes are 3XX for redirects. For those, we need to pass a &lt;code&gt;routerContext&lt;&#x2F;code&gt; object to the &lt;code&gt;&amp;lt;StaticRouter&amp;gt;&lt;&#x2F;code&gt; component and check if &lt;code&gt;routerContext.action &#x3D;&#x3D;&#x3D; &#39;REPLACE&#39;&lt;&#x2F;code&gt; after the virtual DOM is rendered. If so, then &lt;code&gt;react-router&lt;&#x2F;code&gt; would set &lt;code&gt;routerContext.url&lt;&#x2F;code&gt; to the redirect destination&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;user-sessions&quot;&gt;User sessions&lt;&#x2F;h2&gt;
&lt;p&gt;For several hundred words I’ve avoided mentioning an elephant in the room. Even on a streaming service without The Algorithm, there are personalized pages: Watch History, Watch Later, settings. Surely, SSR should deal with user sessions and authentication, right?&lt;&#x2F;p&gt;
&lt;p&gt;Not really&lt;&#x2F;p&gt;
&lt;p&gt;Since the app uses &lt;code&gt;react-router&lt;&#x2F;code&gt;, page navigation after initial load is done client-side and SSR wouldn’t be used. Search engine and social network crawlers won’t be authenticated&lt;&#x2F;p&gt;
&lt;p&gt;So why bother when rendering session-specific &lt;code&gt;&amp;lt;body&amp;gt;&lt;&#x2F;code&gt; on a server would be beneficial only when a human arrives at the site&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:new-tab&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:new-tab&quot; rel&#x3D;&quot;footnote&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, while adding:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;costs — personal responses wouldn’t be cached as often as anonymous ones, so SSR would require more compute resources&lt;&#x2F;li&gt;
&lt;li&gt;lag — responses aren’t as cacheable, so there’s an almost a zero chance that local CDN would be used&lt;&#x2F;li&gt;
&lt;li&gt;risks — having responses for multiple users in the same runtime attracts awful bugs. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;venturebeat.com&#x2F;games&#x2F;steam-goes-down-as-technical-issues-reveal-some-private-info&#x2F;&quot;&gt;Just ask Steam&lt;&#x2F;a&gt;, which had an issue with showing user X data cached for user Y&lt;&#x2F;li&gt;
&lt;li&gt;and, obviously, complexity and more code&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;updated-goal&quot;&gt;Updated goal&lt;&#x2F;h2&gt;
&lt;p&gt;Waaaaait… If we don’t bother with the individual part of the page, we can also cut the rest of the &lt;code&gt;&amp;lt;body&amp;gt;&lt;&#x2F;code&gt;!&lt;&#x2F;p&gt;
&lt;p&gt;SSR in Nebula started as a project for search engines and link previews. Both can read the &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; tag for all the necessary metadata (and, in the case of Google, can execute JS to compute &lt;code&gt;&amp;lt;body&amp;gt;&lt;&#x2F;code&gt; for fuller descriptions and following links)&lt;&#x2F;p&gt;
&lt;p&gt;At the same time, cutting the &lt;code&gt;&amp;lt;body&amp;gt;&lt;&#x2F;code&gt; tag from the SSR response works around the need to hydrate HTML and CSS after browser executes JS — if there’s nothing inside &lt;code&gt;&amp;lt;div id&#x3D;&quot;root&quot;&amp;gt;&amp;lt;&#x2F;div&amp;gt;&lt;&#x2F;code&gt;, nothing would flash or jump around because server thought that visitor has a bigger&#x2F;smaller screen than they actually do. Also, video streaming without JS is &lt;em&gt;possible&lt;&#x2F;em&gt;, but very limiting (both for visitors and developers), so “strict &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;&#x2F;code&gt;” folks are kinda on the outside?..&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-11-Ls4QTeY41Z:maybe-someday&quot; id&#x3D;&quot;rfn:post-2022-11-Ls4QTeY41Z:maybe-someday&quot; rel&#x3D;&quot;footnote&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;So we can safely update SSR’s goal from&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Route requests to our server, render the React app as static HTML, and serve &lt;em&gt;that&lt;&#x2F;em&gt;  &lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;to&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Route requests to our server, render the &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; tag of the React app to static HTML, and serve &lt;em&gt;that&lt;&#x2F;em&gt;  &lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;We still need to render the virtual DOM and query APIs for the &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; tag because it’s rendered with a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;nfl&#x2F;react-helmet&quot;&gt;&lt;code&gt;&amp;lt;Helmet&amp;gt;&lt;&#x2F;code&gt; component&lt;&#x2F;a&gt;. But we can throw away &lt;code&gt;renderedHtml&lt;&#x2F;code&gt; value and do this after we’ve done &lt;code&gt;ReactDOMServer.renderToString&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; helmet &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Helmet&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;renderStatic&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; renderedHtml &#x3D; barebonesIndexHtml
  .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;toString&lt;&#x2F;span&gt;()
  .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;replace&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-regexp&quot;&gt;&#x2F;&amp;lt;html&#x2F;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;&amp;lt;html &#39;&lt;&#x2F;span&gt; + helmet.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;htmlAttributes&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;toString&lt;&#x2F;span&gt;())
  .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;replace&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-regexp&quot;&gt;&#x2F;&amp;lt;title&amp;gt;[^&amp;lt;]*&amp;lt;\&#x2F;title&amp;gt;&#x2F;&lt;&#x2F;span&gt;, helmet.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;title&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;toString&lt;&#x2F;span&gt;())
  .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;replace&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-regexp&quot;&gt;&#x2F;&amp;lt;\&#x2F;head&amp;gt;\s*&amp;lt;body&amp;gt;&#x2F;&lt;&#x2F;span&gt;, [helmet.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;link&lt;&#x2F;span&gt;, helmet.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;meta&lt;&#x2F;span&gt;].&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;join&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;&#39;&lt;&#x2F;span&gt;) + &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#39;&amp;lt;&#x2F;head&amp;gt;&amp;lt;body&amp;gt;&#39;&lt;&#x2F;span&gt;);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This HTML will render &lt;code&gt;&amp;lt;body&amp;gt;&lt;&#x2F;code&gt; with an empty &lt;code&gt;&amp;lt;div id&#x3D;&quot;root&quot;&amp;gt;&amp;lt;&#x2F;div&amp;gt;&lt;&#x2F;code&gt;, but page-specific &lt;code&gt;&amp;lt;title&amp;gt;&lt;&#x2F;code&gt; and Open Graph &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt; tags. Perfect for bots, basically-the-same-as-before for humans&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;deployment&quot;&gt;Deployment&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we have (almost) complete server, we need to deploy it. This topic is outside of the scope of these blog posts. I mean, it’s either “create a &lt;code&gt;Dockerfile&lt;&#x2F;code&gt;” or “do whatever your custom workflow requires you to do”. Docker is boring and written to death, and I have no idea about your custom workflows 🤷&lt;&#x2F;p&gt;
&lt;p&gt;But, if your workflow includes serving static assets with hashed filenames (like &lt;code&gt;index.2c3.js&lt;&#x2F;code&gt;) from S3 or some other object storage, make sure that these both old and new assets are available during server deployment. You wouldn’t want to have a server respond with HTML that mentions &lt;code&gt;index.2c3.js&lt;&#x2F;code&gt; when it hasn’t been uploaded yet (or have been just removed from S3)&lt;&#x2F;p&gt;
&lt;p&gt;To avoid this problem, Nebula’s web deploy workflow keeps static assets on S3 for two calendar years (so, in 2022, there are assets from 2021 and 2022 in the S3 bucket)&lt;&#x2F;p&gt;
&lt;p&gt;Also, if your JS bundle depends on the build &lt;code&gt;.env&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;ENV&lt;&#x2F;code&gt; (for example, &lt;code&gt;create-react-app&lt;&#x2F;code&gt;’s &lt;code&gt;REACT_APP_*&lt;&#x2F;code&gt; environment variables), you’ll need to make it the same when compiling server &lt;em&gt;and&lt;&#x2F;em&gt; client, because while different order of environment variables won’t affect the runtime, it might affect the hash of compiled files, leading to a server expecting &lt;code&gt;index.8ae.js&lt;&#x2F;code&gt; file on S3 instead of just uploaded during the CI&#x2F;CD pipeline &lt;code&gt;index.2c3.js&lt;&#x2F;code&gt;. Approach that worked for us was:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Generate &lt;code&gt;.env&lt;&#x2F;code&gt; during CI&#x2F;CD setup step with the alphabetically ordered keys&lt;&#x2F;li&gt;
&lt;li&gt;Don&#39;t use job-specific environment variables&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id&#x3D;&quot;are-we-done-now&quot;&gt;Are we done &lt;em&gt;now&lt;&#x2F;em&gt;?&lt;&#x2F;h2&gt;
&lt;p&gt;Kinda?.. The server works, bots get the metadata, developers can forget about the parallel repo and focus on the main one when implementing new pages&lt;&#x2F;p&gt;
&lt;p&gt;But have you noticed “(almost)” and multiple mentions of CDN in previous sections? There are more SSR-related things outside of a Node server, and I &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ssr-cdn.html&quot;&gt;will talk about CDNs&lt;&#x2F;a&gt; in another post&lt;&#x2F;p&gt;
&lt;p&gt;Thank you for your attention&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Thanks to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;samwho.dev&#x2F;&quot;&gt;Sam Rose&lt;&#x2F;a&gt; for help with writing this. Photo by &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;@tvick&quot;&gt;Taylor Vick&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:wtf-hooks&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Hooks are a way to update parts of React app’s layout on user or external signal, like “HTTP request is done“. Without them (or old-style “class components” with predefined methods), layout would be either static or updated &lt;em&gt;fully&lt;&#x2F;em&gt;, after receiving and handling a signal at the root element&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:wtf-hooks&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:global-bad&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;IMO, this even without SSR is a code smell, after dealing with a lot of bugs because the “it won’t ever change” constant &lt;em&gt;did&lt;&#x2F;em&gt; change — user connected new input device, changed some browser&#x2F;system setting, etc.&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:global-bad&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:redux&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Speaking as the one who’s rewritten API layer from extremely bolierplate-y Redux&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:redux&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:js-not-ts&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Even though the Nebula web app is written in TypeScript, code snippets here will be in JavaScript for brevity&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:js-not-ts&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:wtf-is-axios&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;axios-http.com&quot;&gt;Axios&lt;&#x2F;a&gt; being our preferred HTTP client, which, before &lt;code&gt;fetch&lt;&#x2F;code&gt; became &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;en&#x2F;blog&#x2F;announcements&#x2F;v18-release-announce&#x2F;#fetch-experimental&quot;&gt;a part of Node&lt;&#x2F;a&gt;, was crucial for sharing query loader function across browsers and the server&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:wtf-is-axios&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:new-tab&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Also, when they reload pages and open internal link in new tabs&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:new-tab&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2022-11-Ls4QTeY41Z:maybe-someday&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Although, &lt;em&gt;some&lt;&#x2F;em&gt; (without any interactivity or video playback) &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;&#x2F;code&gt;-friendly layout might be useful. For example, to preview a video page for those who do enable JS on site-by-site basis?.. 🤔&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-11-Ls4QTeY41Z:maybe-someday&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/ssr-the-server.html</link>
      <guid isPermaLink="false">post-2022-11-Ls4QTeY41Z</guid>
      <pubDate>Sat, 03 Dec 2022 12:40:00 GMT</pubDate>
    </item>
    <item>
      <title>Server-side Rendering: Backstory</title>
      <description>
        &lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;ypgzrjtP7R0&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;720&quot; height&#x3D;&quot;405&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;ypgzrjtP7R0&quot;&gt;&lt;b&gt;The Whys and Hows of Server-side Rendering&lt;&#x2F;b&gt; • BarcelonaJS&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i&gt;
              About implementing SSR at Nebula
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;em&gt;Where I write about the project few months after the fact&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;ypgzrjtP7R0&quot;&gt;Video&lt;&#x2F;a&gt; and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;29fc64FNOQs2myB6ZroANuCTyj.pdf&quot;&gt;slides&lt;&#x2F;a&gt; from the BarcelonaJS talk&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;It all started before I’ve joined &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nebula.tv&quot;&gt;Nebula&lt;&#x2F;a&gt;. During my job interview there, we’ve discussed benefits and problems of Nebula being a single page application. I, who &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;made-a-linkblog.html&quot;&gt;spent &lt;em&gt;some&lt;&#x2F;em&gt; time&lt;&#x2F;a&gt; dealing with &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;ogp.me&quot;&gt;Open Graph&lt;&#x2F;a&gt; for link previews, raised the point that naively implemented, all pages would have the same metadata, which isn’t great for SEO or social sharing. Yeah, Google has enough servers to run JS and compute page’s &lt;code&gt;&amp;lt;title&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt; tags, but I doubted that that’s the case for all messengers, social networks, and blogs&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2022-10-jdjz8iFsUa:1&quot; id&#x3D;&quot;rfn:post-2022-10-jdjz8iFsUa:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;But Twitter and Slack &lt;em&gt;did&lt;&#x2F;em&gt; show those links with nice thumbnails and stuff. Maybe, social networks &lt;em&gt;do&lt;&#x2F;em&gt; run client-side code for Open Graph?..&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;aside-some-basics&quot;&gt;Aside: some basics&lt;&#x2F;h2&gt;
&lt;p&gt;Nebula is a video and podcast streaming service with clients on several platforms. All of them are built on top of common HTTP APIs, some (like Content) are maintained by Nebula team, some (like payments) are third-party&lt;&#x2F;p&gt;
&lt;p&gt;Web version is a &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;reactjs.org&quot;&gt;React&lt;&#x2F;a&gt; application that uses &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;v5.reactrouter.com&quot;&gt;React Router&lt;&#x2F;a&gt; for rendering the correct page content depending on the URL and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;react-query-v3.tanstack.com&quot;&gt;React Query&lt;&#x2F;a&gt; for dealing with HTTP APIs. The application consists of static files, that are built once during release, uploaded to &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;s3&#x2F;&quot;&gt;S3&lt;&#x2F;a&gt;, and served to visitors via &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;cloudfront&#x2F;&quot;&gt;CloudFront&lt;&#x2F;a&gt; CDN&lt;&#x2F;p&gt;
&lt;p&gt;So, back to social networks. Do they run JS to compute Open Graph data?&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;nah&quot;&gt;Nah&lt;&#x2F;h2&gt;
&lt;p&gt;Nope, they don&#39;t. How, then, was Twitter showing thumbnails in Nebula&#39;s link previews? Well, CloudFront distribution from a few lines before wasn’t &lt;em&gt;only&lt;&#x2F;em&gt; serving static files. It also checked request’s &lt;code&gt;User-Agent&lt;&#x2F;code&gt; header and, if it matched the &lt;code&gt;&#x2F;bot|crawler|spider|crawling|facebook|twitter|slack&#x2F;i&lt;&#x2F;code&gt; regex, would forward the request to a &lt;code&gt;nebula-meta&lt;&#x2F;code&gt; server application, which would do necessary requests to Content API and return a barebones version of the page, without any client-side code but with URL-specific &lt;code&gt;&amp;lt;title&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;AC7vOkOf2NCRx4o6WVpiO9cLsQ.png&quot;&gt;
              &lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;AC7vOkOf2NCRx4o6WVpiO9cLsQ&#x2F;fit1600.png&quot; width&#x3D;&quot;1600&quot; height&#x3D;&quot;1022&quot;&gt;
          &lt;&#x2F;a&gt;


        &lt;figcaption&gt;
            &lt;i&gt;
              Nice class page for humans
            &lt;&#x2F;i&gt;
        &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;5klQi9ZfS5IlmR8tGOO5COCTYE.png&quot;&gt;
              &lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;5klQi9ZfS5IlmR8tGOO5COCTYE&#x2F;fit1600.png&quot; width&#x3D;&quot;1600&quot; height&#x3D;&quot;1020&quot;&gt;
          &lt;&#x2F;a&gt;


        &lt;figcaption&gt;
            &lt;i&gt;
              Brutalist class page for bots
            &lt;&#x2F;i&gt;
        &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ul&gt;
&lt;p&gt;The fact that &lt;code&gt;nebula-meta&lt;&#x2F;code&gt; existed was largely forgotten, because APIs still worked, Open Graph didn’t change, and its original developer moved on to other parts of the company. But keeping it was problematic:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;when &lt;code&gt;meta&lt;&#x2F;code&gt; was originally written, Content API was a third-party one (let’s call it Y). Since then, the API became first-party, but we still uploaded images and descriptions to Y, which, at some point, had lead to a head-scratching “stale thumbnail” bug&lt;&#x2F;li&gt;
&lt;li&gt;it knew nothing about then-upcoming Nebula Classes&lt;&#x2F;li&gt;
&lt;li&gt;it knew nothing about WhatsApp, Telegram, random blogs, and other crawlers that might be interested in Open Graph data&lt;&#x2F;li&gt;
&lt;li&gt;it knew nothing about &lt;code&gt;&#x2F;.well-known&#x2F;&lt;&#x2F;code&gt; URLs, that both &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;digital-asset-links&#x2F;v1&#x2F;getting-started&quot;&gt;Android&lt;&#x2F;a&gt; and &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;xcode&#x2F;supporting-associated-domains&#x2F;&quot;&gt;iOS&lt;&#x2F;a&gt; use to associate domains with applications (for things like password suggestions, deeplinks, and SharePlay)&lt;&#x2F;li&gt;
&lt;li&gt;having a separate codebase for bot requests just &lt;em&gt;asks&lt;&#x2F;em&gt; for bugs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;what-then&quot;&gt;What then?&lt;&#x2F;h2&gt;
&lt;p&gt;So we’ve decided to:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;start rendering application&#39;s pages on a server (i.e. implement Server-side Rendering aka SSR) to keep crawler data in sync with browser data, and to make it easier to tweak and to test crawler-first parts of the site&lt;&#x2F;li&gt;
&lt;li&gt;quickly fix small but critical things in &lt;code&gt;meta&lt;&#x2F;code&gt; before diving into &lt;code&gt;1.&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;disable &lt;code&gt;meta&lt;&#x2F;code&gt; after first two points are done&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Doing the &lt;code&gt;2.&lt;&#x2F;code&gt; was a good idea not only because we&#39;ll make visible improvements sooner, but also because most of this post was happening in winter of 2021&#x2F;2022 and russia&#39;s invasion of Ukraine would move dates &lt;em&gt;a bit&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ssr-the-server.html&quot;&gt;the next part&lt;&#x2F;a&gt; I’ll talk about how SSR was actually done&lt;&#x2F;p&gt;
&lt;p&gt;Thank you for your attention&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2022-10-jdjz8iFsUa:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;it for sure isn’t for this blog&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2022-10-jdjz8iFsUa:1&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/ssr-backstory.html</link>
      <guid isPermaLink="false">post-2022-10-jdjz8iFsUa</guid>
      <pubDate>Wed, 02 Nov 2022 11:03:00 GMT</pubDate>
    </item>
    <item>
      
      <description>
        &lt;p&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;EiqFcc_l_Kk&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;640&quot; height&#x3D;&quot;360&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;EiqFcc_l_Kk&quot;&gt;&lt;b&gt;The Prodigy - Invaders Must Die (Official Video)&lt;&#x2F;b&gt; • The Prodigy&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Tour Dates -http:&#x2F;&#x2F;www.theprodigy.comStream or buy all music by The Prodigy - https:&#x2F;&#x2F;prdgy.lnk.to&#x2F;musicOfficial Prodigy merch - https:&#x2F;&#x2F;prdgy.lnk.to&#x2F;store S...
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;p&gt;

        &lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;post-2022-02-yiGE2Srf2e.html&quot;&gt;&amp;infin;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
      </description>
      <link>https://zemlan.in/post-2022-02-yiGE2Srf2e.html</link>
      <guid isPermaLink="false">post-2022-02-yiGE2Srf2e</guid>
      <pubDate>Thu, 24 Feb 2022 05:57:00 GMT</pubDate>
    </item>
    <item>
      <title>Рекомендации, Алгоритмы и Пространство</title>
      <description>
        &lt;p&gt;&lt;em&gt;Людям, которым нравится &lt;code&gt;Рекомендации&lt;&#x2F;code&gt; также нравятся: &lt;code&gt;Ненавидеть_рекомендации&lt;&#x2F;code&gt; и &lt;code&gt;Диссонанс&lt;&#x2F;code&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Автоматические рекомендации, вроде главной страницы Ютуба или персонализированных плейлистов Спотифая, создали в моей голове две противоречащих мысли. Сначала наступила &amp;quot;все видели&#x2F;слышали то же самое, что и я&amp;quot;, которая со временем дополнилась &amp;quot;никто не видел&#x2F;слышал то же самое, что и я&amp;quot;&lt;&#x2F;p&gt;
&lt;p&gt;С первой относительно понятно, особенно если у рекомендованного видео миллионы просмотров. Так &amp;quot;рекомендации&amp;quot; начинают путаться с &amp;quot;популярным&amp;quot;, из-за чего думается, что все вокруг уже посмотрели это видео и теперь к нему можно спокойно отсылаться в разговорах&lt;&#x2F;p&gt;
&lt;p&gt;Но после пары &lt;del&gt;сотен&lt;&#x2F;del&gt; таких отсылок и последующих непонимающих взглядов собеседников, &lt;em&gt;осознаётся&lt;&#x2F;em&gt; смысл слова &amp;quot;персональные&amp;quot; в сочетании &amp;quot;персональные рекомендации&amp;quot;. А раз рекомендации уникальны для меня, то смысл даже &lt;em&gt;пытаться&lt;&#x2F;em&gt; цитировать слова из песни, если в чужом Спотифае этой песни никогда не было?..&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-08-QVPicaLbkP:1&quot; id&#x3D;&quot;rfn:post-2021-08-QVPicaLbkP:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Эти диссонанс и трудности с отсылками&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-08-QVPicaLbkP:2&quot; id&#x3D;&quot;rfn:post-2021-08-QVPicaLbkP:2&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; настолько напрягают, что решил сформулировать их в текст. И в процессе написания, кажется, нашёл какое-никакое решение…&lt;&#x2F;p&gt;
&lt;p&gt;Первая мысль, &amp;quot;все видели&#x2F;слышали то же самое, что и я&amp;quot;, путает автоматические рекомендации с популярностью, потому что традиционные&#x2F;человеческие рекомендации работают именно так. Если люди в общем со мной пространстве (физическом или цифровом), упоминают XYZ, то, чтобы понимать и быть понятым, XYZ стоит посмотреть&#x2F;послушать&lt;&#x2F;p&gt;
&lt;p&gt;Алгоритмы рекомендаций, конечно, используют физическую (страна&#x2F;город) и цифровую (фолловинги) локацию, но есть более эффективное пространство для извлечения максимума внимания&#x2F;времени&#x2F;денег: ментальное&lt;&#x2F;p&gt;
&lt;p&gt;В ментальном пространстве, идею которого я подцепил в замечательной &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;qntm.org&#x2F;scp&quot;&gt;There Is No Antimemetics Division&lt;&#x2F;a&gt;, близость людей определяется близостью мыслей. Люди поблизости в физическом пространстве будут соседями, люди поблизости в ментальном — единомышленниками. Чтобы быть единомышленниками необязательно лично знать друг друга или знать про друг друга. Достаточно иметь общие или схожие идеи. Например, &amp;quot;мне нравится пёсики&amp;quot; или &lt;a href&#x3D;&quot;http:&#x2F;&#x2F;spectator.ru&#x2F;entry&#x2F;6654&quot;&gt;&amp;quot;я боюсь иного&amp;quot;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Да, мне могут быть интересны местные новости или видео определённых каналов. Но что то, что другое закончится намного быстрее, чем идеи и настроения. Плавная трансформация от &amp;quot;игра X&amp;quot; к &amp;quot;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;ru.wikipedia.org&#x2F;wiki&#x2F;%D0%A1%D0%BA%D0%BE%D1%80%D0%BE%D1%81%D1%82%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B8%D0%B3%D1%80%D1%8B&quot;&gt;speedrun&amp;#39;ы&lt;&#x2F;a&gt; игры X&amp;quot; к &amp;quot;speedrun&amp;#39;ы игры Y&amp;quot; более уместна для рекомендаций, чем трансформация от &amp;quot;новости Киева&amp;quot; к &amp;quot;новости Житомира&amp;quot; к &amp;quot;новости Ровно&amp;quot;&lt;&#x2F;p&gt;
&lt;p&gt;Взглянув на алгоритм рекомендаций как на нечто, оперирующее в первую очередь именно в ментальном пространстве, диссонанс развеивается. Все в моём ментальном пространстве видели&#x2F;слышали XYZ, в отличии от людей в физическом&#x2F;цифровом уголке, в котором я оказался в данный момент&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-08-QVPicaLbkP:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Если это, конечно, песня не из начала-середины нулевых. Тогда цитата находится one step closer to the &lt;del&gt;edge&lt;&#x2F;del&gt; understanding&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-08-QVPicaLbkP:1&quot; rev&#x3D;&quot;footnote&quot;&gt;&amp;#8617;&amp;#xfe0e;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-08-QVPicaLbkP:2&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Которыми зачастую мыслю&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-08-QVPicaLbkP:2&quot; rev&#x3D;&quot;footnote&quot;&gt;&amp;#8617;&amp;#xfe0e;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/recommendations-algorithms-space.html</link>
      <guid isPermaLink="false">post-2021-08-QVPicaLbkP</guid>
      <pubDate>Tue, 24 Aug 2021 15:00:00 GMT</pubDate>
    </item>
    <item>
      <title>If you want to drive a submarine, you gotta join the Navy</title>
      <description>
        &lt;p&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;yD_kCKiSkoI&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;720&quot; height&#x3D;&quot;405&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;yD_kCKiSkoI&quot;&gt;&lt;b&gt;Halt and Catch Fire - Intro&#x2F;Opening song&lt;&#x2F;b&gt; • TheWalkingMan19&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              The intro to amc&#39;s new series halt and catch fireHalt and Catch Fire is an American period drama television series created by Christopher Cantwell and Christ...
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;p&gt;&lt;blockquote&gt;
&lt;p&gt;If you want to drive a submarine, you gotta join the Navy.&lt;&#x2F;p&gt;
&lt;p&gt;Yeah, you might have to salute somebody every once in a while, but... it&#39;s better than buying a rickety old diesel boat that sinks you to the bottom of the ocean.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;За шесть лет с выхода &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.imdb.com&#x2F;title&#x2F;tt4436098&#x2F;&quot;&gt;финала второго сезона&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;ru.wikipedia.org&#x2F;wiki&#x2F;%D0%9E%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%B8%D1%81%D1%8C_%D0%B8_%D0%B3%D0%BE%D1%80%D0%B8&quot;&gt;Halt and Catch Fire&lt;&#x2F;a&gt;, эта цитата оттуда продолжает периодически всплывать на ум&lt;&#x2F;p&gt;
&lt;p&gt;Сериал следует за историей и конфликтами вымышленной группы разработчиков и “managerial types”, которые работали где-то рядом с важными для IT событиями в восьмидесятых-девяностых годах: IBM clones, вирусы, онлайн игры, сообщества и коммерция, поисковики и веб-директории…&lt;&#x2F;p&gt;
&lt;p&gt;Цитата в начале записи принадлежит managerial co-founder Донне, которая убеждает engineering co-founder Кэмерон в том, что их компании Mutiny лучше партнёриться с провайдерами (и терпеть их повышения цен), чем покупать себе (полурабочий) сервер. И, пожалуй, она права — когда сервера стоят миллион долларов, то жадные партнёры оправданы…&lt;&#x2F;p&gt;
&lt;p&gt;Но я думаю об этой реплике не в контексте “необходимое оборудование”, а в контексте работы, где “submarine” — это крупный и известный проект, а “Navy” — компания, разрабатывающая этот проект. И, глядя на прошлые несколько лет, быть в подлодке мне неприятно. Да, &lt;em&gt;сами по себе&lt;&#x2F;em&gt; они интересны (одни только &lt;del&gt;атомные реакторы&lt;&#x2F;del&gt; многомиллионные толпы пользователей чего стоят), но внутри них слишком клаустрофобично, да и задачи они решают сомнительные (или сомнительными средствами)&lt;&#x2F;p&gt;
&lt;p&gt;Так что фраза в заголовке этой записи провоцирует вопрос “Do I though?..”. Надёжных недорогих&#x2F;бесплатных инструментов достаточно, возможных задач на небольшую команду (даже из одного человека) уйма. И когда нравится открытое “море” разработки, на поверхности им наслаждаться лучше, чем запертым в металической трубе. Возможно, иногда заплывая в порты для подзарядки и&#x2F;или ныряя за богатствами…&lt;&#x2F;p&gt;

        
      </description>
      <link>https://zemlan.in/hacf-submarine-navy.html</link>
      <guid isPermaLink="false">post-2021-06-A7rfE8NAG1</guid>
      <pubDate>Sun, 27 Jun 2021 11:23:00 GMT</pubDate>
    </item>
    <item>
      <title>everywhere is undefined</title>
      <description>
        &lt;p&gt;&lt;em&gt;Используя скриптовую часть джаваскрипта&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;GfD-MYaWjIU&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;720&quot; height&#x3D;&quot;405&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;GfD-MYaWjIU&quot;&gt;&lt;b&gt;Everywhere is undefined: используя скриптовую часть JavaScript  [ru] &#x2F; Антон Веринов&lt;&#x2F;b&gt; • fwdays&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Видео с онлайн-конференции JavaScript fwdays&#39;21, которая прошла с 1 по 8 июня 2021 года.Описание доклада:Начав свой путь в браузере, джаваскрипт просочился н...
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;em&gt;(адаптировано из выступления на &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;fwdays.com&#x2F;event&#x2F;javascript-fwdays-2021&quot;&gt;JavaScript fwdays’21&lt;&#x2F;a&gt;. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;T1kIIxe0ANWEgs45bzIzZCKZrL.pdf&quot;&gt;слайды&lt;&#x2F;a&gt;)&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Я, как и многие разработчики, склеиваю коллажи из старых систем, нового кода, хотелок пользователей и требований бизнеса. Сами по себе, эти коллажи плохо держатся, поэтому их нужно чем-то скреплять. Так сложилось, что я для этого использую джаваскрипт&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;We aimed to provide a “glue language” for the Web designers and part time programmers who were building Web content from components such as images, plugins, and Java applets. We saw Java as the “component language” used by higher-priced programmers, where the glue programmers — the Web page designers — would assemble components and automate their interactions using JS&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20190322043309&#x2F;https:&#x2F;&#x2F;www.computerworld.com.au&#x2F;article&#x2F;255293&#x2F;a-z_programming_languages_javascript&#x2F;&quot;&gt;The A-Z of Programming Languages: JavaScript - Computerworld&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Это «скрепляющее» свойство было в языке с самого начала, когда с его помощью добавляли щепотку интерактива на страницы. Когда JS был быстро-быстро написан в конце 1995 года, уже существовал способ создания интерактивных элементов на страницу — java applet’ы были представлены несколькими месяцами ранее&lt;&#x2F;p&gt;
&lt;p&gt;Но эти апплеты жили в своём отдельном мирке внутри волшебного HTML-тега и требовали отдельной разработки, тогда как джаваскрипт активно пользовался тем, что браузеры неплохо делали даже двадцать шесть лет назад&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;1995-browser&quot;&gt;1995: Browser&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;em&gt;(все исходники доступны &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&quot;&gt;на гитхабе&lt;&#x2F;a&gt;)&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-html&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;style&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;language-css&quot;&gt;
  &lt;span class&#x3D;&quot;hljs-selector-tag&quot;&gt;body&lt;&#x2F;span&gt; { &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;max-width&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;12em&lt;&#x2F;span&gt;; &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;margin&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1em&lt;&#x2F;span&gt; auto; }
  &lt;span class&#x3D;&quot;hljs-selector-id&quot;&gt;#lastRoll&lt;&#x2F;span&gt; { &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;font-size&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4em&lt;&#x2F;span&gt;; &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;margin&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0.5em&lt;&#x2F;span&gt;; }
&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;style&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&amp;lt;!-- https:&#x2F;&#x2F;xkcd.com&#x2F;221&#x2F; --&amp;gt;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;h1&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;id&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;lastRoll&quot;&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;⚃&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;h1&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Представь, что ты написала &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;anton.codes&#x2F;mo-dice&#x2F;browser&#x2F;noscript.html&quot;&gt;лучший вебсайт&lt;&#x2F;a&gt;. Да, «вебсайт», потому что до «веб-приложений» социум тогда ещё не созрел. Та и приложения тогда «программами» называли… Короче, лучший вебсайт 1995 года — симулятор бросков кубика. Когда ты его писала, ты фокусировалась на главном — как предоставить посетителям &lt;em&gt;самый&lt;&#x2F;em&gt; случайный кубик — а за «доставку» симулятора и за его графический интерфейс пусть отдувается браузер&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-html&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;style&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;language-css&quot;&gt;
  &lt;span class&#x3D;&quot;hljs-selector-tag&quot;&gt;body&lt;&#x2F;span&gt; { &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;max-width&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;12em&lt;&#x2F;span&gt;; &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;margin&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1em&lt;&#x2F;span&gt; auto; }
  &lt;span class&#x3D;&quot;hljs-selector-id&quot;&gt;#lastRoll&lt;&#x2F;span&gt; { &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;font-size&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4em&lt;&#x2F;span&gt;; &lt;span class&#x3D;&quot;hljs-attribute&quot;&gt;margin&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0.5em&lt;&#x2F;span&gt;; }
&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;style&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&amp;lt;!-- https:&#x2F;&#x2F;xkcd.com&#x2F;221&#x2F; --&amp;gt;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;h1&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;id&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;lastRoll&quot;&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;⚃&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;h1&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;input&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;type&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;button&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;id&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;roll&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;value&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;🔄&quot;&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;input&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;script&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;language-javascript&quot;&gt;
  roll.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;onclick&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
    lastRoll.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;innerText&lt;&#x2F;span&gt; &#x3D; [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚀&quot;&lt;&#x2F;span&gt;,&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚁&quot;&lt;&#x2F;span&gt;,&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚂&quot;&lt;&#x2F;span&gt;,&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚃&quot;&lt;&#x2F;span&gt;,&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚄&quot;&lt;&#x2F;span&gt;,&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚅&quot;&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;parseInt&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;random&lt;&#x2F;span&gt;() * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;6&lt;&#x2F;span&gt;)]
  }
&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;script&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Через некоторое время ты слышишь, что многие посетители разоряются на телефонных счетах, потому что ради свежего рандома им нужно держать соединение к Интернету активным. И конечно же, вместо реимплементации и логики, и UI в джава апплете, ты &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;anton.codes&#x2F;mo-dice&#x2F;browser&#x2F;somescript.html&quot;&gt;добавляешь кнопку и небольшой скрипт&lt;&#x2F;a&gt;, который дружит её с текстом на странице&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;20xx&quot;&gt;20XX&lt;&#x2F;h3&gt;
&lt;p&gt;За последующие годы, ты понемножку рефакторишь теперь-уже-веб-приложение — добавляешь историю бросков, немного динамизма, начинаешь использовать новые фишки джаваскрипта, вроде классов и модулей — но его главный принцип, «статическая страница с щепоткой интерактива», &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;anton.codes&#x2F;mo-dice&#x2F;browser&#x2F;index.html&quot;&gt;продолжает отлично работать&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-javascript&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;R&lt;&#x2F;span&gt; &#x3D; (&lt;span class&#x3D;&quot;hljs-params&quot;&gt;n&lt;&#x2F;span&gt;) &#x3D;&amp;gt; &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;parseInt&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;random&lt;&#x2F;span&gt;() * n);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;class&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt; {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;static&lt;&#x2F;span&gt; symbols &#x3D; [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚀&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚁&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚂&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚃&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚄&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;⚅&quot;&lt;&#x2F;span&gt;];

  &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;constructor&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;options&lt;&#x2F;span&gt;) {
    options &#x3D; options || {};

    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt; &#x3D; options.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt; || &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;;
    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt; &#x3D;
      &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;typeof&lt;&#x2F;span&gt; options.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;number&quot;&lt;&#x2F;span&gt;
        ? &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; 0 &amp;lt; options.lastRoll &amp;lt; this.symbols.length&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;max&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;min&lt;&#x2F;span&gt;(options.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;length&lt;&#x2F;span&gt; - &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt;))
        : &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;R&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;length&lt;&#x2F;span&gt;);
    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt; &#x3D; options.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt; || [];
  }

  &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;roll&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;R&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;length&lt;&#x2F;span&gt;);
    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt; &#x3D; [&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt;, ...&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt;].&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;slice&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;10&lt;&#x2F;span&gt;);
  }

  &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;pretty&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; {
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;lastRoll&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;[&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt;],
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;history&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;map&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;e&lt;&#x2F;span&gt;) &#x3D;&amp;gt;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;symbols&lt;&#x2F;span&gt;[e]),
    };
  }

  &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;clear&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;this&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt; &#x3D; [];
  }
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;export&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;default&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt;;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;data:text&#x2F;html;base64,PHRpdGxlPm1vLWRpY2U8L3RpdGxlPgo8bWV0YSBwcm9wZXJ0eT0ib2c6aW1hZ2UiIGNvbnRlbnQ9Ii9tZWRpYS81Qmc1dXdCbVBmRWJBMWdpSk9KZlBRNks5dy5wbmciPgo8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CjxzdHlsZT4KICBib2R5IHsgbWF4LXdpZHRoOiA1MDBweDsgbWFyZ2luOiAxMHB4IGF1dG87IGJhY2tncm91bmQ6IHdoaXRlOyBmb250LXNpemU6IDQwcHg7IH0KICBidXR0b24geyBmb250LXNpemU6IDFlbTsgfQogICNsYXN0Um9sbCB7IG1hcmdpbjogMC41ZW07IH0KICAjbG9nIHsgcGFkZGluZzogMDsgfQogICNsb2cgbGkgeyBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7IG1hcmdpbi1yaWdodDogMC41ZW07IH0KPC9zdHlsZT4KCjwhLS0gaHR0cHM6Ly94a2NkLmNvbS8yMjEvIC0tPgo8aDEgaWQ9Imxhc3RSb2xsIj7imoM8L2gxPgo8YnV0dG9uIGlkPSJyb2xsIj7wn5SEPC9idXR0b24+CjxidXR0b24gaWQ9ImNsZWFyIj7wn5quPC9idXR0b24+Cjx1bCBpZD0ibG9nIj48L3VsPgoKPHNjcmlwdCB0eXBlPSJtb2R1bGUiPgogIGNvbnN0IFIgPSAobikgPT4gcGFyc2VJbnQoTWF0aC5yYW5kb20oKSAqIG4pOwoKY2xhc3MgQ29yZSB7CiAgc3RhdGljIHN5bWJvbHMgPSBbIuKagCIsICLimoEiLCAi4pqCIiwgIuKagyIsICLimoQiLCAi4pqFIl07CgogIGNvbnN0cnVjdG9yKG9wdGlvbnMpIHsKICAgIG9wdGlvbnMgPSBvcHRpb25zIHx8IHt9OwoKICAgIHRoaXMuc3ltYm9scyA9IG9wdGlvbnMuc3ltYm9scyB8fCBDb3JlLnN5bWJvbHM7CiAgICB0aGlzLmxhc3RSb2xsID0KICAgICAgdHlwZW9mIG9wdGlvbnMubGFzdFJvbGwgPT09ICJudW1iZXIiCiAgICAgICAgPyAvLyAwIDwgb3B0aW9ucy5sYXN0Um9sbCA8IHRoaXMuc3ltYm9scy5sZW5ndGgKICAgICAgICAgIE1hdGgubWF4KDAsIE1hdGgubWluKG9wdGlvbnMubGFzdFJvbGwsIHRoaXMuc3ltYm9scy5sZW5ndGggLSAxKSkKICAgICAgICA6IFIodGhpcy5zeW1ib2xzLmxlbmd0aCk7CiAgICB0aGlzLmhpc3RvcnkgPSBvcHRpb25zLmhpc3RvcnkgfHwgW107CiAgfQoKICByb2xsKCkgewogICAgdGhpcy5sYXN0Um9sbCA9IFIodGhpcy5zeW1ib2xzLmxlbmd0aCk7CiAgICB0aGlzLmhpc3RvcnkgPSBbdGhpcy5sYXN0Um9sbCwgLi4udGhpcy5oaXN0b3J5XS5zbGljZSgwLCAxMCk7CiAgfQoKICBwcmV0dHkoKSB7CiAgICByZXR1cm4gewogICAgICBsYXN0Um9sbDogdGhpcy5zeW1ib2xzW3RoaXMubGFzdFJvbGxdLAogICAgICBoaXN0b3J5OiB0aGlzLmhpc3RvcnkubWFwKChlKSA9PiB0aGlzLnN5bWJvbHNbZV0pLAogICAgfTsKICB9CgogIGNsZWFyKCkgewogICAgdGhpcy5oaXN0b3J5ID0gW107CiAgfQp9CgogIGNvbnN0IGNvcmUgPSBuZXcgQ29yZSgpOwoKICBmdW5jdGlvbiByZW5kZXJSb2xsKGNvcmUpIHsKICAgIGNvbnN0IGxhc3RSb2xsID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcigiI2xhc3RSb2xsIik7CiAgICBsYXN0Um9sbC5pbm5lckhUTUwgPSBjb3JlLnByZXR0eSgpLmxhc3RSb2xsOwogICAgbGFzdFJvbGwuc3R5bGUudHJhbnNmb3JtID0KICAgICAgYHRyYW5zbGF0ZSgke01hdGgucmFuZG9tKCkgLSAwLjV9ZW0sICR7TWF0aC5yYW5kb20oKSAtIDAuNX1lbSlgOwogIH0KCiAgZnVuY3Rpb24gcmVuZGVySGlzdG9yeShjb3JlKSB7CiAgICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCIjbG9nIikuaW5uZXJIVE1MID0gY29yZQogICAgICAucHJldHR5KCkKICAgICAgLmhpc3RvcnkubWFwKCh2KSA9PiBgPGxpPiR7dn08L2xpPmApCiAgICAgIC5qb2luKCIiKTsKICB9CgogIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoIiNyb2xsIikuYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLCAoKSA9PiB7CiAgICBjb3JlLnJvbGwoKTsKICAgIHJlbmRlclJvbGwoY29yZSk7CiAgICByZW5kZXJIaXN0b3J5KGNvcmUpOwogIH0pOwoKICBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCIjY2xlYXIiKS5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsICgpID0+IHsKICAgIGNvcmUuY2xlYXIoKTsKICAgIHJlbmRlckhpc3RvcnkoY29yZSk7CiAgfSk7Cjwvc2NyaXB0Pg&#x3D;&#x3D;&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;720&quot; height&#x3D;&quot;405&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          mo-dice&lt;br&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;hr&gt;
&lt;p&gt;То, что браузеры следуют более-менее общему стандарту &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.ecma-international.org&#x2F;publications-and-standards&#x2F;standards&#x2F;ecma-262&#x2F;&quot;&gt;ECMAScript’а&lt;&#x2F;a&gt;,  обусловлено, кроме всего прочего, тем, что им надо показывать одни и те же сайты и, следовательно, запускать одни и те же скрипты. Если какой-то популярный сайт не работает, то тухлые помидоры полетят не только в его сторону, но и в сторону браузера. Если сайт &lt;em&gt;достаточно&lt;&#x2F;em&gt; популярный, то это разработчики браузера кому придётся адаптировать движок под сайт, а не наоборот. Как это делают, например, разработчики Вебкита, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;WebKit&#x2F;WebKit&#x2F;blob&#x2F;36582be546b17e05d68968fa67f5ec0dd9a4b156&#x2F;Source&#x2F;WebCore&#x2F;page&#x2F;Quirks.cpp&quot;&gt;&quot;исправляя&quot; движок под кучу сайтов&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Но не у всего софта, который запускает джаваскрипт, есть требование универсальности. Скрипты под этот софт пишутся &lt;em&gt;только&lt;&#x2F;em&gt; под этот софт, так что где-то можно проигнорировать синтаксический сахар, где-то можно предоставить другие глобальные объекты&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Одним примером такого софта является nginx. nginx — это веб-сервер, который часто служит как reverse proxy, прослойка между диким интернетом и компьютерами, на которых крутятся веб-приложения. Это прослойка может раздавать статические файлы, направлять запросы на наименее нагружённые сервера, управлять правами доступа вроде «пускай на &lt;em&gt;этот&lt;&#x2F;em&gt; секретный адрес только посетителей с &lt;em&gt;этих&lt;&#x2F;em&gt; айпишников», и заниматься прочими «сисадминскими» штуками&lt;&#x2F;p&gt;
&lt;p&gt;Традиционно, если пользователям nginx’а не хватает стандартных средств конфигурации, они используют &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.lua.org&quot;&gt;lua&lt;&#x2F;a&gt; для написания кастомной логики. lua отлично для этого подходит, но её с ней знакомо &lt;em&gt;намного&lt;&#x2F;em&gt; меньше народу, чем с джаваскриптом. Поэтому команда nginx’а добавила в свой сервер поддержку рантайма и диалекта джаваскрипта под названием &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nginx.org&#x2F;en&#x2F;docs&#x2F;njs&#x2F;&quot;&gt;njs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Lua is a good tool in this area, but it’s not as widely known as some other languages.&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.nginx.com&#x2F;blog&#x2F;launching-nginscript-and-looking-ahead&#x2F;&quot;&gt;Launching nginScript and Looking Ahead - NGINX&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;em&gt;В теории&lt;&#x2F;em&gt;, они могли бы использовать один из браузерных рантаймов и автоматически получить все прелести современного джаваскрипта. Но браузеры больше оптимизированы под долгоживущие страницы, а не под короткие сниппеты кода, выполняемые на пути перед основным сервером. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.nginx.com&#x2F;blog&#x2F;nginscript-why-our-own-javascript-implementation&#x2F;&quot;&gt;Поэтому&lt;&#x2F;a&gt; команда nginx сделала свой рантайм, в котором реализовала отдельные фичи стандарта&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;2015-nginx&quot;&gt;2015: nginx&lt;&#x2F;h2&gt;
&lt;p&gt;Будучи ранним адоптером веба, ты, конечно же, завела блог, он же «набор статических файлов, спрятанных за nginx’ом». Из-за твоего прошлого успеха с симулятором кубика, ссылка на блог попадает в круг &lt;em&gt;&quot;успешных&quot;&lt;&#x2F;em&gt; людей, которые начинают спамить комментариями о том, как купить крипту и стать такой же успешной. Чтобы спрятаться от всего этого успеха, ты решаешь пускать к блогу только самых невезучих. Тех, кому кубик всегда показывает единичку&lt;&#x2F;p&gt;
&lt;p&gt;Ты берёшь ядро симулятора кубика и делаешь на его основе революционную схему авторизации. Так как джаваскрипт у nginx’а отличается от браузерного, тебе понадобится немного напильника…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Так, например, nginx вполне неплохо &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&#x2F;blob&#x2F;main&#x2F;nginx&#x2F;core.js&quot;&gt;справляется без классов&lt;&#x2F;a&gt;. А действительно, зачем классы, когда для обработки «куда отправить запрос?» не нужны сотни объектов, каждый со своим стейтом и десятками методов. А если уже приспичит, то и олдового прототипного &lt;code&gt;var Class &#x3D; function() {}&lt;&#x2F;code&gt; + &lt;code&gt;Class.prototype&lt;&#x2F;code&gt; хватит. В то же время, разделение кода с помощью ES модулей более оправдана даже для короткоживущих скриптов&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;(конечно, большую часть напильника можно заменить автоматической транспиляцией, но это выступление не о настройке вебпака)&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;После адаптации ядра под другой джаваскрипт, нужно и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&#x2F;blob&#x2F;main&#x2F;nginx&#x2F;modice.js&quot;&gt;самим немного адаптироваться под реалии сервера&lt;&#x2F;a&gt;, где у каждого из &lt;em&gt;десятков&lt;&#x2F;em&gt; посетителей своя история и ей неплохо жить между перезагрузками нашего сервера. Ядро отлично сериализируется в строку, так что можно сохранять его у куки каждого посетителя&lt;&#x2F;p&gt;
&lt;p&gt;Для загрузки состояния из кук нужно парсить эти самые куки. К сожалению (или к счастью) njs не заморачивался с поддержкой &lt;code&gt;npm&lt;&#x2F;code&gt; и &lt;code&gt;node_modules&lt;&#x2F;code&gt;, поэтому придётся &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;rendro&#x2F;525bbbf85e84fa9042c2&quot;&gt;копипастить всякое&lt;&#x2F;a&gt;. Лиииибо, можно воспользоваться тем, что предоставляет окружение, и достать уже &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&#x2F;blob&#x2F;b914510d6115bd4fae16aeb7843637b67a7c3ef3&#x2F;nginx&#x2F;modice.js#L8-L9&quot;&gt;распаршенное значение из nginx’овых переменных&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Imzob2KQrcMO7ZbA3Cw55Fzuwk.mp4&quot; poster&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Imzob2KQrcMO7ZbA3Cw55Fzuwk&#x2F;firstframe.jpeg&quot; controls&#x3D;&quot;&quot; preload&#x3D;&quot;none&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;2017-google-docs&quot;&gt;2017: Google Docs&lt;&#x2F;h2&gt;
&lt;p&gt;Продав свой Dice-as-a-service многотриллионой корпорации за миллиарды денег, но оставив за собой права на использование бесценного ядра, ты начинаешь осваивать бухгалтерию. Так как именно веб оплатил твой бутерброд с маслом, бухгалтерию ты ведёшь в Google Таблицах&lt;&#x2F;p&gt;
&lt;p&gt;Ты активно используешь &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.google.com&#x2F;docs&#x2F;table&#x2F;25273?hl&#x3D;en&quot;&gt;кучу стандартный функций&lt;&#x2F;a&gt; Таблиц, для банальной арифметики, обработки текста, статистики, удалённых запросов к гугловым сервисам  (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.google.com&#x2F;docs&#x2F;answer&#x2F;3093281&quot;&gt;&lt;code&gt;GOOGLEFINANCE&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;) или сервисам в остальном интернете (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.google.com&#x2F;docs&#x2F;answer&#x2F;3093342&quot;&gt;&lt;code&gt;IMPORTXML&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;Однако, точность формул в таблицах тебе со временем надоедает. Ты хочешь вернуться во времена шального рандома…&lt;&#x2F;p&gt;
&lt;p&gt;Поэтому ты открываешь &lt;code&gt;Tools &amp;gt; Script editor&lt;&#x2F;code&gt; и создаёшь там два файлика — &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&#x2F;blob&#x2F;main&#x2F;spreadsheets&#x2F;core.gs&quot;&gt;один с любимым ядром&lt;&#x2F;a&gt;, другой — &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;mo-dice&#x2F;blob&#x2F;main&#x2F;spreadsheets&#x2F;roll.gs&quot;&gt;с кастомной функцией &lt;code&gt;ROLL&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;То, что MS Office делает Visual Basic’ом, Google Docs делает джаваскриптом. С его помощью можно генерировать слайды презентации, делать автозамены в текстовых документах, или создавать кастомные функции для вычислений в таблицах&lt;&#x2F;p&gt;
&lt;p&gt;В целом, функции в Таблицах вызываются так же, как в джаваскрипте (и других Си-подобных языках), за парой исключений:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;префикс &lt;code&gt;&#x3D;&lt;&#x2F;code&gt;, чтобы дать гуглу понять, что у ячейки именно вычисляемое значение, а не текст&lt;&#x2F;li&gt;
&lt;li&gt;в качества переменных используются имена (&lt;code&gt;A1&lt;&#x2F;code&gt;) и диапазоны (&lt;code&gt;B2:C3&lt;&#x2F;code&gt;) ячеек&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Каждая функция может вернуть либо один результат, либо двухмерный массив значений (тогда изменится значение текущей и прилегающих к ней ячеек)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-javascript&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;**
 * Rolls a die with custom faces.
 *
 * &lt;span class&#x3D;&quot;hljs-doctag&quot;&gt;@param&lt;&#x2F;span&gt; {&lt;span class&#x3D;&quot;hljs-type&quot;&gt;string|Array&amp;lt;Array&amp;lt;string&amp;gt;&amp;gt;&lt;&#x2F;span&gt;} faces The value or range of cells
 *     to use as a die faces.
 * &lt;span class&#x3D;&quot;hljs-doctag&quot;&gt;@return&lt;&#x2F;span&gt; A die roll.
 * &lt;span class&#x3D;&quot;hljs-doctag&quot;&gt;@customfunction&lt;&#x2F;span&gt;
 *&#x2F;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;ROLL&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;faces&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;let&lt;&#x2F;span&gt; symbols;

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Array&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;isArray&lt;&#x2F;span&gt;(faces)) {
    symbols &#x3D; faces.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;reduce&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;acc, row&lt;&#x2F;span&gt;) &#x3D;&amp;gt;&lt;&#x2F;span&gt; [...acc, ...row], []).&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;filter&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Boolean&lt;&#x2F;span&gt;);
  } &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;else&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (faces) {
    symbols &#x3D; faces.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;toString&lt;&#x2F;span&gt;().&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;split&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&lt;&#x2F;span&gt;);
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; core &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt;({
    symbols,
  });
  core.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;roll&lt;&#x2F;span&gt;();
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; core.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;pretty&lt;&#x2F;span&gt;().&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Функция &lt;code&gt;ROLL&lt;&#x2F;code&gt; будет принимать в качестве аргумента либо строку со всеми гранями кубика, либо двухмерный массив граней. Так как кастомные скрипты запускаются в V8 на серверах Гугла, то можно использовать довольно современный синтаксис и фичи джаваскрипта, вне зависимости от твоего браузера. За исключением модулей, которые здесь заменены глобальным неймспейсом для всех файлов «скрипта»&lt;&#x2F;p&gt;
&lt;p&gt;Теперь, если указать в ячейке значение &lt;code&gt;&#x3D;ROLL(&quot;fwdays&quot;)&lt;&#x2F;code&gt;, то функция наградит тебя случайным допамином на основе &lt;code&gt;&quot;fwdays&quot;&lt;&#x2F;code&gt;’а. Или &lt;code&gt;javascript&lt;&#x2F;code&gt;’а&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;BuCte4f12Mtne7mlIUYRRXILza&#x2F;fit1600.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Как можно заметить, кастомная функция никак не взаимодействует с прошлыми бросками. Так как кастомные функции каждый раз запускаются в чистом окружении, состояние ядра нельзя сохранить «в памяти», как это делалось в браузере&lt;&#x2F;p&gt;
&lt;p&gt;Кроме стандартной библиотеки, у скриптов есть доступ к &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;apps-script&#x2F;guides&#x2F;sheets&#x2F;functions#advanced&quot;&gt;дополнительным сервисам&lt;&#x2F;a&gt;, вроде HTTP-запросов, парсинга XML’я, или чисто-гугловых переводов и навигации&lt;&#x2F;p&gt;
&lt;p&gt;В теории, можно было бы использовать &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;apps-script&#x2F;reference&#x2F;cache&quot;&gt;сервис кэширования&lt;&#x2F;a&gt;, который представляет из себя хранилище ключей-значений, привязанных к скрипту и&#x2F;или документу и&#x2F;или пользователю&lt;&#x2F;p&gt;
&lt;p&gt;Но он довольно бесполезен для кубиков, потому что, насколько я знаю, кастомные функции запускаются только на изменения входных параметров (будь то константы или значения ячеек), а не каких-то внешних (или внутренний) триггеров вроде «эта функция была вызвана в другой ячейке»&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;2018-ios&quot;&gt;2018: iOS&lt;&#x2F;h2&gt;
&lt;p&gt;Остановившись на «незапоминающихся» бросках кубиков и насладившись случайными таблицами, ты подвела бюджеты и поняла, что можешь позволить себе айфон. Ты слышала про чрезвычайно умную Siri и, сразу после распаковки и настройки, попросила её бросить кубик. И снова. И снова. Весь этот допамин!&lt;&#x2F;p&gt;
&lt;p&gt;Но после того, как телефон дважды подряд выбросил тройку, ты поняла, что тебе не хватает поздравлений с удачей, а Siri оказалась слишком забывчива. Старенькое ядро умеет хранить историю бросков. Ах, если бы была возможность подружить его с Siri…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Нативный софт на большинстве эппловых платформ имеет доступ к &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;javascriptcore&quot;&gt;JavascriptCore&lt;&#x2F;a&gt; рантайму, которым активно пользуются все приложения на React Native. К счастью, этот рантайм подходит не только для кривого повторения нативного UI, но и для взаимодействия с нативными API вроде &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;widgetarian-wmuwme.html&quot;&gt;виджетов&lt;&#x2F;a&gt;, Share Sheet’а, Shortcuts.app, или Siri. Приложение &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;scriptable.app&quot;&gt;Scriptable&lt;&#x2F;a&gt; занимается именно этим, запуская произвольный джаваскрипт в окружении с байндингами к нативным функциям iOS&lt;&#x2F;p&gt;
&lt;p&gt;JSC, а значит и Scriptable, поддерживает современный синтаксис, так что ядро можно скопировать из браузера как есть, за исключением &lt;code&gt;export&lt;&#x2F;code&gt;’а, который надо будет заменить на типа-CommonJS’овый &lt;code&gt;module.exports &#x3D;&lt;&#x2F;code&gt;, чтобы потом импортировать ядро предоставленной Scriptable’ом функцией &lt;code&gt;importModule()&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-javascript&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;importModule&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;core.js&quot;&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; fileManager &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;FileManager&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;local&lt;&#x2F;span&gt;();
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; cachePath &#x3D; fileManager.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;joinPath&lt;&#x2F;span&gt;(
  fileManager.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;documentsDirectory&lt;&#x2F;span&gt;(), &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;modice.json&quot;&lt;&#x2F;span&gt;
);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;load&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; symbols &#x3D; [
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;единицу&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;двойку&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;тройку&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;четвёрку&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;пятёрку&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;шестёрку&quot;&lt;&#x2F;span&gt;,
  ];

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (fileManager.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;fileExists&lt;&#x2F;span&gt;(cachePath)) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;try&lt;&#x2F;span&gt; {
      &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt;({
        ...&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;JSON&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;parse&lt;&#x2F;span&gt;(fileManager.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;readString&lt;&#x2F;span&gt;(cachePath)),
        symbols,
      });
    } &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;catch&lt;&#x2F;span&gt; (e) {
      &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;error&lt;&#x2F;span&gt;(e);
    }
  }
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Core&lt;&#x2F;span&gt;({ symbols });
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;save&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;core&lt;&#x2F;span&gt;) {
  fileManager.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeString&lt;&#x2F;span&gt;(cachePath, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;JSON&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;stringify&lt;&#x2F;span&gt;(core));
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getSpokenRoll&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;core&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;let&lt;&#x2F;span&gt; nth &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;;

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;for&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; entry &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;of&lt;&#x2F;span&gt; core.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;history&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (core.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lastRoll&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; entry) {
      nth &#x3D; nth + &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt;;
    } &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;else&lt;&#x2F;span&gt; {
      &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;break&lt;&#x2F;span&gt;;
    }
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; numeralWord &#x3D; [
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;второй&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;третий&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;четвёртый&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;пятый&quot;&lt;&#x2F;span&gt;,
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;шестой&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;седьмой&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;восьмой&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;девятый&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;десятый&quot;&lt;&#x2F;span&gt;,
  ][nth];
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; numeralWord
    ? &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;Кубик &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${numeralWord}&lt;&#x2F;span&gt; раз показал &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${core.pretty().lastRoll}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;
    : &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;Кубик показал &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${core.pretty().lastRoll}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;;
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; core &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;load&lt;&#x2F;span&gt;();

core.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;roll&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;save&lt;&#x2F;span&gt;(core);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; response &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getSpokenRoll&lt;&#x2F;span&gt;(core);

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(response);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (config.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;runsWithSiri&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Speech&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;speak&lt;&#x2F;span&gt;(response);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Для сохранения состояния ядра между запусками, можно читать&#x2F;писать в локальную файловую систему или в iCloud. Из-за того, что доступа к iCloud’у может не быть&lt;&#x2F;p&gt;
&lt;p&gt;Далее, необходимо собрать фразу, которой Siri будет отвечать на твои запросы. Раз кубик будет существовать только на словах, то и его грани могут быть словами, которые будут произноситься. В &lt;code&gt;numeralWord&lt;&#x2F;code&gt; записываем сколько раз подряд был показан последний результат. Склеиваем всё вместе, и отдаём это в &lt;code&gt;Speech.speak()&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Теперь остаётся только обозначить, что этот скрипт надо запускать в ответ на волшебное слово&lt;&#x2F;p&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;UVkisyI8ZhLEjGYT8kpifj0SzQ.mp4&quot; poster&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;vcdVafW0yr9NOWJXkCk74ulBoQ&#x2F;fit1600.png&quot; controls&#x3D;&quot;&quot; preload&#x3D;&quot;none&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;everywhere&quot;&gt;&lt;code&gt;everywhere&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Кроме уже перечисленных браузеров, nginx, Google Docs и Scriptable, джаваскрипт можно запускать приблизительно везде и делать им приблизительно всё:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;конечно же, консольные и серверные приложения (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;en&#x2F;&quot;&gt;Node.js&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;deno.land&quot;&gt;Deno&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;тесты (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;pptr.dev&quot;&gt;puppeteer&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.cypress.io&quot;&gt;cypress&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;дополнения к браузерам (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developer.chrome.com&#x2F;docs&#x2F;extensions&#x2F;&quot;&gt;Chrome&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;addons.mozilla.org&#x2F;en-US&#x2F;developers&#x2F;&quot;&gt;Firefox&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;safari&#x2F;extensions&#x2F;&quot;&gt;Safari&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;кастомные функции в базах данных (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;abiliojr&#x2F;sqlite-js&quot;&gt;sqlite-js&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;plv8.github.io&quot;&gt;plv8&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;CDN (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;workers.cloudflare.com&quot;&gt;Cloudflare Workers&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;автоматизация macOS (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;awesome-demos-done-quick.html&quot;&gt;JXA&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.scriptkit.com&quot;&gt;ScriptKit&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;rsnous&#x2F;status&#x2F;1259614184897015808&quot;&gt;PDF!&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;железки (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.espruino.com&#x2F;Pixl.js&quot;&gt;Pixl.js&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Этот список уже сейчас неполный, а может быть ещё &lt;em&gt;более&lt;&#x2F;em&gt; неполным, если ты заскриптуешь свой нативный софт. Если V8&#x2F;JavascriptCore сильно тяжёлые, то есть другие JS движки, вроде &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;duktape.org&quot;&gt;duktape&lt;&#x2F;a&gt; и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;bellard.org&#x2F;quickjs&#x2F;&quot;&gt;quickjs&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;и-так-что-же-мы-сегодня-узнали&quot;&gt;И так, что же мы сегодня узнали…&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;g3SfxCCZD026MM9PnMQjoYdnqh&#x2F;gifv.mp4&quot; poster&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;g3SfxCCZD026MM9PnMQjoYdnqh&#x2F;fit1000.png&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Что джаваскрипт джаваскрипту рознь, но если какого-нибудь синтаксиса в &lt;em&gt;этом&lt;&#x2F;em&gt; JS окружении нет, то это необязательно баг или недостаток. Возможно, этот синтаксис бесполезен. Или окружение решает ту же проблему по-своему&lt;&#x2F;p&gt;
&lt;p&gt;Что в джаваскрипт приходят люди из разных уголков разработки, каждый со своими ожиданиями и привычками&lt;&#x2F;p&gt;
&lt;p&gt;И, наконец, что зачастую лучше довериться пользователям и дать им возможность самим заскриптовать нужную только им фичу, чем пытаться угадать все необходимые конфигурации&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Спасибо &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;croftyland&quot;&gt;Кристине Ландвитович&lt;&#x2F;a&gt; за помощь в написании этой записи&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;

        
      </description>
      <link>https://zemlan.in/everywhere-is-undefined.html</link>
      <guid isPermaLink="false">post-2021-05-uovl7jiTNz</guid>
      <pubDate>Thu, 03 Jun 2021 20:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Склепал линкблог</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;LpLdc8jPEOarqatm3pAxAn8ZpF&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot; width&#x3D;&quot;1426&quot; height&#x3D;&quot;771&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;…из скуки и открытых стандартов&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;За годы своего существования, этот бложек пережил несколько инкарнаций — на Blogger’е, на WordPress’е, на nanograbbr’е&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-04-pHxoAZQ6EZ:ng&quot; id&#x3D;&quot;rfn:post-2021-04-pHxoAZQ6EZ:ng&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, на Tumblr’е…&lt;&#x2F;p&gt;
&lt;p&gt;Когда пришло время сползать с последнего, в центре внимания были статические генераторы вроде Jekyll, Hugo и Wintersmith. Они неплохие, но идея управлять контентом через Git&#x2F;Github мне как-то не нравилась. Ещё и надо было бы что-то выдумывать с картинками и видео — не добавлять же их в Git&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-04-pHxoAZQ6EZ:lfs&quot; id&#x3D;&quot;rfn:post-2021-04-pHxoAZQ6EZ:lfs&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;В итоге напилил &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;&quot;&gt;scroll&lt;&#x2F;a&gt; — генератор с веб-мордой, который работает с парой sqlite баз и, кроме рендеринга markdown’а в HTML, хранит и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;backstage&#x2F;convert.js&quot;&gt;конвертит&lt;&#x2F;a&gt; картинки в blob’ах. Одних картинок оказалось мало и, когда &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;awesome-demos-done-quick.html&quot;&gt;понадобилось&lt;&#x2F;a&gt; встраивать видео, научил генератор отображать &lt;code&gt;![](https:&#x2F;&#x2F;example.com&#x2F;video.mp4)&lt;&#x2F;code&gt; не как &lt;code&gt;&amp;lt;img&amp;gt;&lt;&#x2F;code&gt;, а как &lt;code&gt;&amp;lt;video&amp;gt;&lt;&#x2F;code&gt;. А где видео, там и Youtube, так что сделал под него special case, чтобы штуки вроде &lt;code&gt;![](https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;np3QLrHJmRA)&lt;&#x2F;code&gt; заменялись на встроенный видеоплеер Ютуба&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;np3QLrHJmRA&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;480&quot; height&#x3D;&quot;360&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;np3QLrHJmRA&quot;&gt;&lt;b&gt;MARINA AND THE DIAMONDS - Obsessions [Official Music Video]&lt;&#x2F;b&gt; • MARINA&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Official Music Video | MARINA AND THE DIAMONDS - ObsessionsMy new album Love + Fear is out now - https:&#x2F;&#x2F;marina.lnk.to&#x2F;loveandfearSubscribe to the MARINA You...
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Затем захотелось встроить на страницу аудиоплеер Apple Music’а, но не хотелось самостоятельно писать подобные замены под каждый сайт. Хотелось…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;embeds&quot;&gt;embeds&lt;&#x2F;h2&gt;
&lt;p&gt;…чтобы движок сам понимал, какую картинку показать, какой плеер загрузить. Благо, эта же хотелка была и у фейсбука с твиттером, которые убедили кучу сайтов и сервисов подобавлять на свои страницы &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt; теги с необходимой для красивых превьюшек информацией для реализации &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;ogp.me&quot;&gt;Open Graph’а&lt;&#x2F;a&gt;.  Мне же оставалось только распарсить всё это добро и сгенерировать подходящие &lt;code&gt;&amp;lt;img&amp;gt;&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;lt;video&amp;gt;&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;lt;audio&amp;gt;&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&amp;lt;iframe&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Чтобы не приходилось ждать внешних HTTP-запросов на каждую регенерацию сайта, неплохо было бы кэшировать ответы. Тут факт использования sqlite сильно упростил задачу — намного проще сохранять ответы в какую-никакую базу, а не в текстовые файлы, которые ещё надо было бы коммитить в репозиторий…&lt;&#x2F;p&gt;
&lt;p&gt;Так движок начал &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;backstage&#x2F;embeds.js#L936-L947&quot;&gt;проверять&lt;&#x2F;a&gt; content-type всех &lt;code&gt;![](типа картинок)&lt;&#x2F;code&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;backstage&#x2F;embeds.js#L1569-L1595&quot;&gt;сохранять&lt;&#x2F;a&gt; &lt;code&gt;og:&lt;&#x2F;code&gt; теги (если это не картинка, а &lt;code&gt;text&#x2F;html&lt;&#x2F;code&gt;), и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;backstage&#x2F;embeds.js#L1399&quot;&gt;генерировать&lt;&#x2F;a&gt; красивые карточки на основе всех этих данных&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-04-pHxoAZQ6EZ:too-damn-big&quot; id&#x3D;&quot;rfn:post-2021-04-pHxoAZQ6EZ:too-damn-big&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. В результате, такой markdown:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; ![](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;open.spotify.com&#x2F;track&#x2F;7tyt9usWqEPPDbyL6CgBs4&lt;&#x2F;span&gt;)
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; ![](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;coub.com&#x2F;view&#x2F;2pc24rpb&lt;&#x2F;span&gt;)
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; ![](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;giphy.com&#x2F;gifs&#x2F;tiktok-dogs-puppy-cute-dog-l2uluGTvB7DAQvZyHp&lt;&#x2F;span&gt;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;генерирует такой контент:&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
            &lt;img class&#x3D;&quot;audio-control&quot; data-src&#x3D;&quot;https:&#x2F;&#x2F;p.scdn.co&#x2F;mp3-preview&#x2F;dc89da6f56bc6933cc30ea9988a59628ade4c201?cid&#x3D;162b7dc01f3a4a2ca32ed3cec83d1e02&amp;amp;utm_medium&#x3D;facebook&quot; alt&#x3D;&quot;The Complexity of Light&quot; src&#x3D;&quot;https:&#x2F;&#x2F;i.scdn.co&#x2F;image&#x2F;ab67616d0000b2731019045b575b58a13be01123&quot;&gt;

      &lt;audio controls&#x3D;&quot;&quot; preload&#x3D;&quot;metadata&quot; src&#x3D;&quot;https:&#x2F;&#x2F;p.scdn.co&#x2F;mp3-preview&#x2F;dc89da6f56bc6933cc30ea9988a59628ade4c201?cid&#x3D;162b7dc01f3a4a2ca32ed3cec83d1e02&amp;amp;utm_medium&#x3D;facebook&quot;&gt;&lt;&#x2F;audio&gt;

      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;open.spotify.com&#x2F;track&#x2F;7tyt9usWqEPPDbyL6CgBs4&quot;&gt;&lt;b&gt;The Complexity of Light&lt;&#x2F;b&gt; • Spotify&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Children of Nova · Song · 2009
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;coub.com&#x2F;embed&#x2F;2pc24rpb&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;720&quot; height&#x3D;&quot;405&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;coub.com&#x2F;view&#x2F;2pc24rpb&quot;&gt;&lt;b&gt;PARADISE BEACH&lt;&#x2F;b&gt; • Coub&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              by Ilya Trushin
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;media0.giphy.com&#x2F;media&#x2F;l2uluGTvB7DAQvZyHp&#x2F;giphy.mp4&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot; width&#x3D;&quot;448&quot; height&#x3D;&quot;450&quot;&gt;&lt;&#x2F;video&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;media0.giphy.com&#x2F;media&#x2F;l2uluGTvB7DAQvZyHp&#x2F;giphy.gif&quot;&gt;&lt;b&gt;Golden Retriever Dogs GIF by TikTok - Find &amp;amp; Share on GIPHY&lt;&#x2F;b&gt; • TikTok&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Discover &amp;amp; share this TikTok GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;Может, стоит завернуть эту часть scroll’а в простенький сервис?..&lt;&#x2F;em&gt; 🤔&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;всё-в-rss&quot;&gt;Всё в RSS&lt;&#x2F;h2&gt;
&lt;p&gt;Параллельно, вне исходников scroll’а, начал собирать хоть сколько-то долгий контент в RSS. Я уже &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;paperss.html&quot;&gt;рассказывал&lt;&#x2F;a&gt; про &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;paperss&#x2F;&quot;&gt;paperss&lt;&#x2F;a&gt;, который генерирует фид из Instapaper’а. Кроме него, в RSS перетащил ещё и свои Youtube-подписки, подальше от заманчивых лабиринтов рекомендаций&lt;&#x2F;p&gt;
&lt;p&gt;И всё это время думал о том, как бы делиться хорошими ссылками. Не &lt;em&gt;отличными&lt;&#x2F;em&gt;, для которых не лень написать пять-десять слов описания и запостить в твиттер&#x2F;слак, а именно &lt;em&gt;хорошими&lt;&#x2F;em&gt;. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;feedbin.com&quot;&gt;Feedbin&lt;&#x2F;a&gt;, которым я синхронизирую RSS-подписки, позволяет отмечать записи звёздочками, из которых он даже генерирует отдельный RSS-фид&lt;&#x2F;p&gt;
&lt;p&gt;Это &lt;em&gt;почти&lt;&#x2F;em&gt; то, что мне нужно, но:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;отмеченное звёздочками живёт где-то сильно вне этого блога&#x2F;твиттера&#x2F;чего-то ещё;&lt;&#x2F;li&gt;
&lt;li&gt;RSS-фид содержит полные записи и было бы некрасиво, по отношению к оригинальным авторам, дублировать их у себя;&lt;&#x2F;li&gt;
&lt;li&gt;одних только названия и ссылки кажется маловато&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Ах если бы был способ красиво показать ссылки на другие сайты…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;наконец-таки-линкблог&quot;&gt;Наконец-таки линкблог&lt;&#x2F;h2&gt;
&lt;p&gt;Подружив opengraph’овые “карточки” с Feedbin’овскими звёздочками, в которые входят статьи из Instapaper’а, получилась и простая в создании, и приятно выглядящая &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;linkblog.html&quot;&gt;лента ссылок&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Для меня, интерфейс работы с линкблогом полностью&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-04-pHxoAZQ6EZ:almost&quot; id&#x3D;&quot;rfn:post-2021-04-pHxoAZQ6EZ:almost&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; спрятан в RSS-ридере: читаю статьи и смотрю видео так же, как всегда, но теперь нажатие на ⭐️ менее бесполезное и создаёт немного работы для scroll’а, который:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;раз в приблизительно полчаса, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;linkblog.js#L232-L256&quot;&gt;проверяет&lt;&#x2F;a&gt; RSS-фид Feedbin’а&lt;&#x2F;li&gt;
&lt;li&gt;если там появился новый &lt;em&gt;контент&lt;&#x2F;em&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;linkblog.js#L66-L91&quot;&gt;сохраняет его в базу&lt;&#x2F;a&gt; и заново генерирует необходимые страницы (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;linkblog.js#L145-L173&quot;&gt;&lt;code&gt;linkblog.html&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;linkblog.js#L175-L202&quot;&gt;&lt;code&gt;feeds&#x2F;linkblog.xml&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;generate-post.js#L154-L158&quot;&gt;&lt;code&gt;index.html&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;в процессе генерации этих страниц, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;scroll&#x2F;blob&#x2F;78bafc1403fd79a91c19440a7e5c8e281551356c&#x2F;linkblog.js#L134-L141&quot;&gt;загружает&lt;&#x2F;a&gt; для каждой ссылки необходимые &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt; теги&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;B8Xl0JpxRDWQqMzC2REv7i5UwA.mp4&quot; poster&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;B8Xl0JpxRDWQqMzC2REv7i5UwA&#x2F;firstframe.jpeg&quot; controls&#x3D;&quot;&quot; preload&#x3D;&quot;none&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Больше всего проблем было с тем, как же показывать линкблог на главной странице — стандартные карточки слишком большие и отвлекали бы от основных записей, а в уменьшенные версии влезает слишком мало текста. В итоге остановился на том, что на главной странице показываю только картинки&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;linkblog.html&quot;&gt;Красота&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-04-pHxoAZQ6EZ:ng&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Кажется, про него не осталось ничего, кроме &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;groups.google.com&#x2F;g&#x2F;nanograbbr&quot;&gt;древних переписок в Google Groups&lt;&#x2F;a&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-04-pHxoAZQ6EZ:ng&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-04-pHxoAZQ6EZ:lfs&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Да, да, есть &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-lfs.github.com&quot;&gt;Git LFS&lt;&#x2F;a&gt;, но 🤫 &lt;br&gt; &lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;AxMjoIgO9kqXwgcKaANvoQUwak&#x2F;gifv.mp4&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot;&gt;&lt;&#x2F;video&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-04-pHxoAZQ6EZ:lfs&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-04-pHxoAZQ6EZ:too-damn-big&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Неплохо бы разбить на части этот здоровенный файл и причесать его, но лень. Да и не то, чтобы кому-то, кроме меня, понадобится в нём что-то править. А если хочешь сказать, что код ужасный и мне следует его стыдиться, смотри гифку в предыдущей сноске&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-04-pHxoAZQ6EZ:too-damn-big&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-04-pHxoAZQ6EZ:almost&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Ну ладно, &lt;em&gt;почти&lt;&#x2F;em&gt; полностью. Если ссылка не из подписок, а из твиттера или переписок, то для публикации в линкблог, её надо сначала добавить в Instapaper. Ну и в закулисьи блога есть кнопка для ручной проверки обновлений&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-04-pHxoAZQ6EZ:almost&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/made-a-linkblog.html</link>
      <guid isPermaLink="false">post-2021-04-pHxoAZQ6EZ</guid>
      <pubDate>Mon, 19 Apr 2021 12:57:00 GMT</pubDate>
    </item>
    <item>
      <title>Code review &amp; response</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Z0Ce7K9w3oLjSbi0XaXtQ1LeSi&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;aka «The Hard Part»&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Колега написала код, опублікувала та описала його. Тобі прийшло повідомлення, що вона призначила code review на тебе. Що робити далі?&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Серія записів про code review:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;Навіщо?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;Створення pull request’у&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-assignee.html&quot;&gt;Думки про pull request assignee&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;Code review &amp;amp; response&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Англійською: &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;on-code-reviews.html&quot;&gt;On Code Reviews&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;слухати&quot;&gt;Слухати&lt;&#x2F;h2&gt;
&lt;p&gt;У кожної зміни в pull request’і є ціль. Окрім очевидних зовнішніх «виправити баг», «додати фічу»&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:1&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; і «оновити код&#x2F;залежності», ціль може бути внутрішньою, наприклад:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;бути проміжним кроком у виправлені багу&#x2F;додаванні фічі («ця функція експортується, тому що вона використовується в неопублікованому коді»)&lt;&#x2F;li&gt;
&lt;li&gt;вирівняти існуючий код з ментальною моделлю у голові авторки pull request’у («воно було незрозуміло, я переписала, щоб стало зрозуміліше»)&lt;&#x2F;li&gt;
&lt;li&gt;підвищити зручність розробки («я вмію робити &lt;code&gt;grep&lt;&#x2F;code&gt;, а цей файл цьому чинив сильний опір»)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Ревʼювер може не погоджуватися з цінністю кожної окремої цілі або шляху її досягнення, але для гарного code review треба їх &lt;em&gt;знати&lt;&#x2F;em&gt; і, за необхідністю, вирівнювати їх з цілями решти проєкту&lt;&#x2F;p&gt;
&lt;p&gt;Тому перед тим, як шаленіти через &quot;говнокоду&quot; (&lt;em&gt;що б це не значило&lt;&#x2F;em&gt;), задумайся над ціллю, яку переслідувала авторка. З зовнішніми цілями тобі допоможуть текст задачі або фонові обговорення. Внутрішні ж найчастіше можна зрозуміти хіба що за контекстом в історії комітів або в описі pull request’у&lt;&#x2F;p&gt;
&lt;p&gt;Якщо ж контексту авторка залишила небагато, то можна допомогти собі  &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;collaborating-with-issues-and-pull-requests&#x2F;filtering-files-in-a-pull-request&quot;&gt;фільтрацією змінених файлів&lt;&#x2F;a&gt;, переглядом &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;searching-for-information-on-github&#x2F;searching-commits#search-by-author-or-committer&quot;&gt;її минулих змін&lt;&#x2F;a&gt;, локальним підняттям гілки pull request’у, спробою переписати «дивне рішення» з урахуванням «ну, це ж так не робиться»…&lt;&#x2F;p&gt;
&lt;p&gt;Ну, або у &lt;em&gt;краааааайньому&lt;&#x2F;em&gt; випадку, можна запитати її напряму&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;говорити&quot;&gt;Говорити&lt;&#x2F;h2&gt;
&lt;p&gt;Кожен коментар ревʼювера, так само як і кожна зміна у pull request’і, служить для чогось. Наприклад, за допомогою коментарів, ревʼювер може…&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;дізнаватися&quot;&gt;Дізнаватися&lt;&#x2F;h3&gt;
&lt;p&gt;«Жанр» коментарів, який є найбільш наближеним до &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;головного призначення code review&lt;&#x2F;a&gt;, але який рідко використовують, це «Чому написано саме &lt;em&gt;так&lt;&#x2F;em&gt;?». Навіть коли усі учасники дискусії знають причину, але вона ніде не згадана явно, необхідність сформулювати відповідь допоможе помітити надмірну складність&lt;&#x2F;p&gt;
&lt;p&gt;Якщо вони думають, що знають причину, то явне формулювання точки зору однієї з учасниць code review може виявити випадки, коли ці знання є лише припущеннями. А припущення ведуть до багів, тож іноді краще уточнити щось «очевидне». Якщо ж це «очевидне» вже було десь розписаним, то нічого жахливого у тому, щоб отримати посилання на коментар&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:2&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:2&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; з відповіддю або на коміт з поясненням&lt;&#x2F;p&gt;
&lt;p&gt;На жаль, цей жанр не лише найважливіший (на мою думку), але й найнебезпечніший. Прочитавши «Чому написано саме &lt;em&gt;так&lt;&#x2F;em&gt;?» у поганому настрої, я можу накрутити себе думками «та як це вони сумніваються у моїй праці?!» та&#x2F;або «схоже, тут &lt;em&gt;так&lt;&#x2F;em&gt; неможна, тому, замість відповіді на питання, я усе зараз же перепишу». Тому дуже важливо дати авторці pull request’у зрозуміти, що у твоєму питанні немає ніякого прихованого сенсу. У тексті інтонація передається надзвичайно погано. Тож, щоб текстом коментар читався настільки ж позитивно, як якби його було почуто вголос, цю недостачу інтонації треба компенсувати. Наприклад, перефразувавши питання, виділивши наголос курсивом, явно написавши «пробач, я тут ні про що не натякаю», або додавши краплю емоцій емоджами&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;sarahcandersen.com&#x2F;post&#x2F;618916011346968576&quot;&gt;
              &lt;img alt&#x3D;&quot;Sarah&#39;s
Scribbles&quot; src&#x3D;&quot;&#x2F;media&#x2F;WDgPiySzF4naFcqy5kOXwkIuLJ.jpeg&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;sarahcandersen.com&#x2F;post&#x2F;618916011346968576&quot;&gt;&lt;b&gt;Sarah&#39;s
Scribbles&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Tumblr Blog
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;h3 id&#x3D;&quot;коригувати&quot;&gt;Коригувати&lt;&#x2F;h3&gt;
&lt;p&gt;Набагато популярніший жанр коментарів до pull request’ів — це «тут неправильно». Багато хто вважає, що ці коментарі — найважливіше у code review. Так, додаткова пара очей допоможе виявити проблеми раніше, але, повертаючись до початку цього запису, виключно важливо розуміти цілі змін перед тим, як їх критикувати. Без цього розуміння, обидві сторони code review будуть псувати настрій одна одній — або шкідливими порадами, або поверхневими правками для «відстань»&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;blockquote cite&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;
        Chesterton&#39;s Fence:&lt;br&gt;&lt;br&gt;&quot;If you don&#39;t see the use of it, I certainly won&#39;t let you clear it away. Go away and think. Then, when you can come back and tell me that you do see the use of it, I may allow you to destroy it.&quot;
      &lt;&#x2F;blockquote&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;larsiusprime&#x2F;status&#x2F;1344531640140361728&quot;&gt;&lt;b&gt;Lars &quot;Sweet Leaf&quot; Doucet on Twitter&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;І навіть якщо ти знаєш, яку ціль переслідувала авторка в &lt;em&gt;цій&lt;&#x2F;em&gt; зміні, то…&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:unemployable&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:unemployable&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Зовсім необовʼязково просити виправити кожен рядок, в якому бачив проблему. По-перше, проблеми може не бути, тоді «чому?» спрацює краще. По-друге, особистий досвід запамʼятовується краще чужого, тож якщо проблема некритична та&#x2F;або швидко проявиться та&#x2F;або легко сховається від користувачів, то десять хвилин паніки навчать &lt;em&gt;багато чому&lt;&#x2F;em&gt;. Наприклад, сім років тому, я так навчився що нові фічі треба ховати за &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Feature_toggle&quot;&gt;feature toggle&lt;&#x2F;a&gt; і що кеш треба інвалідувати&lt;&#x2F;p&gt;
&lt;p&gt;Коли ж проблема дійсно варта коментаря, то усі схожі коментарі краще залишити за раз, щоб не затягувати час до закриття pull request’у очікуванням фіксів&#x2F;коментарій. Іншими словами, «коментар-коментар-фікс-фікс» краще за «коментар-фікс-коментар-фікс»&lt;&#x2F;p&gt;
&lt;p&gt;Окрім вказівки на проблему, ревʼювер може запропонувати її вирішення. Github дозволяє &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;collaborating-with-issues-and-pull-requests&#x2F;incorporating-feedback-in-your-pull-request#applying-a-suggested-change&quot;&gt;зручно це зробити&lt;&#x2F;a&gt; прямо у веб-морді&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;pDBf0XDoacZCL9S2PfbvmbRTd1.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;spJeelkeTI379teRTvOQV2x1LO.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Якщо маєш гарні відносини з авторкою pull request’у і ви не сумніваєтеся у здібностях одна одної, то можна склонувати собі гілку і запропонувати покращення у вигляді комітів. Куди пушати ці покращення — у нову гілку (з відкриттям pull request’у до pull request’у&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:obligatory-inception-joke&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:obligatory-inception-joke&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;) або в ту ж саму — залежить від того, як до цього відноситься авторка оригінальної гілки. Це, як і «дозволити помилитися», доволі ризикований шлях, але він існує&lt;&#x2F;p&gt;
&lt;p&gt;Повертаючись до втрати інтонацій в тексті… Якщо людина може неправильно зрозуміти невинне «чому?», то «я написав код для &lt;em&gt;твого&lt;&#x2F;em&gt; pull request’у» або яскраво червоне «&lt;code&gt;@username&lt;&#x2F;code&gt; requested changes» точно рано чи пізно будуть прийняті особисто. Тож їх треба навмисно помʼякшувати або хоча б нагадувати усім, що це критика &lt;em&gt;коду&lt;&#x2F;em&gt;, а не людини&lt;&#x2F;p&gt;
&lt;p&gt;Але навіть «&lt;code&gt;@username&lt;&#x2F;code&gt; requested changes» краще тиши&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;форматувати&quot;&gt;&lt;del&gt;Форматувати&lt;&#x2F;del&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Що не треба коментувати, так це крапки-з-комою, відступи та інше форматування. Якщо форматування коду дійсно важливе, то замість написання коментаря «ми ставимо пробіли всередині фігурних дужок», витрати свої зусилля на просте автоматичне виправлення форматування&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:no-config&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:no-config&quot; rel&#x3D;&quot;footnote&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; і на запуск лінтеру в CI-середовищі&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-fqryZPk7jx:hooks-arent-enough&quot; id&#x3D;&quot;rfn:post-2021-03-fqryZPk7jx:hooks-arent-enough&quot; rel&#x3D;&quot;footnote&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Без цього, ревʼюверу треба постаратися, щоб утримати фокус на сенсі pull request’у і не перемикатися на коментування поверхневих та часто марних для кінцевих користувачів «відсортуй імпорти за абеткою»&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;мати-на-увазі&quot;&gt;Мати на увазі&lt;&#x2F;h3&gt;
&lt;p&gt;Коментарі ревʼюверів можуть бути адресовані не виключно авторці pull request’у, але і іншим колегам&lt;&#x2F;p&gt;
&lt;p&gt;Наприклад, щоб звернути увагу до складності інтеграції з API іншої частини системи. Або до більшої-ніж-хотілося-б кількості мануальних змін там, де рефакторінг&#x2F;типи&#x2F;автоматизація спростили б та скоротили б майбутні pull request’и. Або до схожості нового коду на код у іншому проєкті і до можливого code share&lt;&#x2F;p&gt;
&lt;p&gt;Одразу реалізувати ці коментарі, у тому ж pull request’і, скоріш за все, зайве збільшить його scope, але, через пару таких коментарів-нагадувань, буде легше згадати про них підчас планування наступного спрінта&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;заохочувати&quot;&gt;Заохочувати&lt;&#x2F;h3&gt;
&lt;p&gt;Якщо після відкриття pull request’ів отримувати лише описані раніше нейтральні та відносно негативні коментарі, то &lt;em&gt;звичайно&lt;&#x2F;em&gt; люди будуть уникати code review — «Я намагалася, працювала, оминала проблеми, тільки щоб перед самим фінішем слухати, що тут дивно, тут неправильно, тут погано…»&lt;&#x2F;p&gt;
&lt;p&gt;Тому, у противагу всьому цьому, корисно хоча б іноді хвалити особливо хороші зміни, будь то витончене вирішення проблеми, гарна ідея (яку ревʼювер може застосувати у своєму коді), або банальне «дякую, давно болить, але руки ніяк не доходили»&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Без коментарів, code review — це лише вахтерство. З коментарями лише з серії «тут неправильно, виправ» — шкидливий gatekeeping. Сподіваюсь, цей запис продемонстрував, що з правильними коментарями, pull requestʼи стають набагато корисніше всім його учасникам&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Дякую Богдану Бузу та Олександру Вєрінову за допомогу у написанні цього запису&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Перформанс та документація також є фічами&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:1&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:2&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;І, ймовірно, уїдливе «варто б було б це знати» від людини, яка сама тільки вчора про це дізналася&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:2&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:unemployable&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;У наступного абзацу далеко не нульовий ризик зробити мене ще більш unemployable ніж я є зараз&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:unemployable&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:obligatory-inception-joke&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;G2jUhnCU9iA&quot;&gt;BRAAAAM&lt;&#x2F;a&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:obligatory-inception-joke&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:no-config&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Бажано з мінімумом налаштувань, щоб не було спокуси витрачати час на життєво важливих питань на кшталт «подвійні лапки чи одинарні?». наприклад, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;eslint&#x2F;eslint&#x2F;blob&#x2F;8984c91372e64d1e8dd2ce21b87b80977d57bff9&#x2F;conf&#x2F;eslint-recommended.js&quot;&gt;&lt;code&gt;eslint:recommended&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;prettier.io&#x2F;&quot;&gt;&lt;code&gt;prettier&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;standardjs.com&#x2F;&quot;&gt;&lt;code&gt;standard&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; в джаваскриптовій екосистемі, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;black.readthedocs.io&#x2F;en&#x2F;stable&#x2F;&quot;&gt;&lt;code&gt;black&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; в пітонячій, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;golang.org&#x2F;cmd&#x2F;gofmt&#x2F;&quot;&gt;&lt;code&gt;gofmt&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; в Go, та &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;editorconfig.org&#x2F;&quot;&gt;&lt;code&gt;.editorconfig&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; приблизно усюди&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:no-config&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-03-fqryZPk7jx:hooks-arent-enough&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;І, будь ласка, не виправдовуй відсутність лінтеру на CI наявністю git hook’ів. Хуки можуть не запускатися з тієї чи іншої причини, наприклад коли коміт було зроблено через веб-морду Github’у. Тож, без лінтеру на CI, головна гілка буде з поламаним форматуванням частіше ніж бажалося б (а бажалося б «ніколи»)&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-fqryZPk7jx:hooks-arent-enough&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/code-review-response.html</link>
      <guid isPermaLink="false">post-2021-03-fqryZPk7jx</guid>
      <pubDate>Mon, 22 Mar 2021 10:52:00 GMT</pubDate>
    </item>
    <item>
      <title>Думки про pull request assignee</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;0eSXpBAnhrHMabQo8qahYSXwBF&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Уважно обирай авдиторію&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;В кінці &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;запису про створення pull request’у&lt;&#x2F;a&gt;, я мимохіть згадав assignee&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Додай в assignee зацікавлених колег (будь то окремі люди або цілі команди)&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Нібито це настільки легко та настільки малозначно…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Серія записів про code review:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;Навіщо?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;Створення pull request’у&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-assignee.html&quot;&gt;Думки про pull request assignee&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;Code review &amp;amp; response&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Англійською: &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;on-code-reviews.html&quot;&gt;On Code Reviews&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;you-are-reviewer-zero&quot;&gt;You are Reviewer Zero&lt;&#x2F;h2&gt;
&lt;p&gt;Нульовим ревʼювером є сам автор pull request’у. Звичайно, ніхто не заважає забити і переключитися на іншу задачу після того, як передав естафету колегам, але перегляд знайомого коду в новому середовищі (UI code review вместо UI code editor’а) або через деякий час (коли усі «очевидності» забулися) допоможе самостійно помітити помилку або який-небудь дивний момент &lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Я иногда делаю ещё так - после публикации своего PR я сам же ему делаю Review (в Github можно прокомментировать свой PR, но без Approve). Это позволяет добавить комментарии&#x2F;контекст конкретно к тому коду, которого это касается&lt;&#x2F;p&gt;
&lt;p&gt;— гарний коментар до попереднього запису&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Особливо зручно це робити у &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.blog&#x2F;2019-02-14-introducing-draft-pull-requests&#x2F;&quot;&gt;чорновому pull request’і&lt;&#x2F;a&gt;, який нібито вже створено та має перевірки на CI, але ще не повідомив про своє існування &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;creating-cloning-and-archiving-repositories&#x2F;about-code-owners&quot;&gt;code owner’ам&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;кому-дивиться&quot;&gt;Кому дивиться&lt;&#x2F;h2&gt;
&lt;p&gt;У невеликих проєктах з щільною командою, питання «кому кинути на ревʼю» вирішується сам собою — ти або кидаєш усім (тому що вас небагато), або ти знаєш відповідальну за ту чи іншу частину коду і кидаєш їй&lt;&#x2F;p&gt;
&lt;p&gt;В більш… &lt;em&gt;динамічних&lt;&#x2F;em&gt; проєктах та командах, через розміри та поганий звʼязок, ситуація може бути складнішою. Найчастіше, це вирішено згадкою у code owner’ах або цілих команд, або «володарів думки» (тімліди&#x2F;техліди&#x2F;сініори&#x2F;тощо). Обидва шляхи створюють свої проблеми&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;команди-у-code-ownerах&quot;&gt;Команди у code owner’ах&lt;&#x2F;h3&gt;
&lt;p&gt;Команда у ревʼюверах створює ризик того, що кожен учасник команди вважатиме що перегляд твого pull request’у — чуже завдання. Наявність «чергових»&#x2F;«oncall» в команді не рятує — якщо pull request було створено у вечір пʼятниці, коли я був черговим, то у понеділок я думатиму «тепер це проблема сьогоднішньої чергової Олени» і забити. Олена, в свою чергу, також може справедливо забити — pull request же пʼятничний, а в пʼятницю чергувала не вона&lt;&#x2F;p&gt;
&lt;p&gt;Плюс, новачки в команді можуть соромитися коментувати — раптом щось не помітять або поставлять «дурне» питання. А якщо в ревʼюверах уся команда, то можна причаїтися і перечекати&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;NkgYr8RSbndFNZiYNCd4d7iqXy.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Команда в assignee може мати і протилежний ефект, коли усі-усі-усі поспішають зробити коментар, не вдаючись у подробиці&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-03-e2CbqzAJ16:tnx&quot; id&#x3D;&quot;rfn:post-2021-03-e2CbqzAJ16:tnx&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. В теорії, автор pull request’у може вкласти час та зусилля на відповідь усім &quot;роззявакам&quot;, але це складно, виснажує, і може потребувати виправдань начальству, якщо воно вважає що «твоя задача — вирішувати проблеми з кодом, а не вчити джунів в інших командах»&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;володарі-думки-у-code-ownerах&quot;&gt;Володарі думки у code owner’ах&lt;&#x2F;h3&gt;
&lt;p&gt;Наявність когось, через кого будуть проходити усі pull request’и, звичайно, має свої плюси, наприклад, більш консистентний стиль і вища шанс помітити конфлікт з неявним припущенням в проєкті&lt;&#x2F;p&gt;
&lt;p&gt;Але безглуздо очікувати (або вимагати), що одна людина буде уважно пропускати через себе увесь контекст з тією ж швидкістю, з якою решта команди пише код. У трикутнику «багато&#x2F;швидко&#x2F;уважно» обрати усі три неможна. Навіть два не завжди вдається…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;5iTt8tIMx5DBhGyJE4hQq9vKLt.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Я не знаю, як однозначно вирішити обидві проблеми. Але впевнений, що:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;на кожен окремий pull request краще призначати не усю команду, а окремих людей, навіть якщо єдиний їх внесок буде «перепризначити більш відповідній людині»&lt;&#x2F;li&gt;
&lt;li&gt;дуже важливо уникати створення людини-bottleneck’а, яка недовго протримається під тиском начальства та колег&lt;&#x2F;li&gt;
&lt;li&gt;дивитись pull request’и мають не лише ліди&#x2F;сініори, але і джуни. Інакше без джунівського «що тут відбувається?» усе відлетить у стратосферу на абстракціях, а спростити код у момент code review набагато простіше, ніж після нього&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr&gt;
&lt;p&gt;Після цієї примітки, тепер &lt;em&gt;точно&lt;&#x2F;em&gt; можна говорити &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;про процес з точки зору ревʼюверів&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-03-e2CbqzAJ16:tnx&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Дякую &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;wowitskatya&quot;&gt;Каті&lt;&#x2F;a&gt; за нагадування&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-03-e2CbqzAJ16:tnx&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/code-review-assignee.html</link>
      <guid isPermaLink="false">post-2021-03-e2CbqzAJ16</guid>
      <pubDate>Sun, 07 Mar 2021 12:18:00 GMT</pubDate>
    </item>
    <item>
      <title>Створення pull request’у</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;NyVRViO3aJO44aRjXyFYNGjj7z&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The Repo With A Thousand Faces&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Серія записів про code review:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;Навіщо?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;Створення pull request’у&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-assignee.html&quot;&gt;Думки про pull request assignee&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;Code review &amp;amp; response&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Англійською: &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;on-code-reviews.html&quot;&gt;On Code Reviews&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;При написанні pull request’у &lt;em&gt;(PR)&lt;&#x2F;em&gt;, корисно керуватися принципом «вклади на оформлення стільки ж часу та&#x2F;або зусиль, скільки хочеш щоб витратили на ревʼю»&lt;&#x2F;p&gt;
&lt;p&gt;Якщо автор не вкладе час на те, щоб зібрати усе до купи, тоді ревʼювери матимуть це зробити у голові. Навіть якщо ревʼювери &lt;em&gt;знають&lt;&#x2F;em&gt; про особливість у коді, вони можуть про неї не згадати підчас читання PR, тому що голова зайнята мережею звʼязків, які автор вирішив не полірувати&lt;&#x2F;p&gt;
&lt;p&gt;Якщо автор проґавить у описі PR момент, який змусив його обрати менше зло, ревʼювер так саме проґавить цей момент підчас написання коментарів&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;github-stories&quot;&gt;Github Stories&lt;&#x2F;h2&gt;
&lt;p&gt;Як я писав &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;минулого разу&lt;&#x2F;a&gt;, PR’и створюються для отримання досвіду та&#x2F;або думки зі сторони. Для того щоб колега могла скласти цю саму думку, для початку вона має зрозуміти, що ж відбувається у коді. Знаючи це, можна зробити правильний або неправильний висновок:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;неправильний висновок: «якщо колеги розуміють — пощастило, якщо ні — це повністю їх проблема»&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ul&gt;
&lt;li&gt;неправильний висновок: «я можу зробити PR менше або більше зрозумілим»&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Для того, щоб колезі було легше сприймати PR і ділитися доречним досвідом, їй треба знати «як було раніше», «що змінюється», «чому саме це має змінитися», «як змінюється» и «чим стає». Зміни безпосередньо коду відповідають на деякі з цих питань&lt;&#x2F;p&gt;
&lt;p&gt;Наприклад, ліва&#x2F;червона частина diff’у відповідає на «що змінюється», а права&#x2F;зелена — на «чим стає». З рештою питань треба занурюватися у прозу&lt;&#x2F;p&gt;
&lt;p&gt;Якщо ця проза — це сухий набір фактів, то ревʼювери матимуть самостійно їх звʼязувати. Щоб спростити це, автор PR може обʼєднати їх у історію про &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;uk.wikipedia.org&#x2F;wiki&#x2F;%D0%9C%D0%BE%D0%BD%D0%BE%D0%BC%D1%96%D1%84&quot;&gt;транcформацію&lt;&#x2F;a&gt;, у якій головний герой — це репозиторій, до якого відкрито PR&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;eqPmK0r3leV5SEE4sCDnbdOWDl.png&quot; alt&#x3D;&quot;&quot; width&#x3D;&quot;1000&quot; height&#x3D;&quot;958&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ми розуміємо і витягуємо «суть» з історій, тому що роками чуємо&#x2F;читаємо&#x2F;дивимося їх. Плюс, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.sciencedaily.com&#x2F;releases&#x2F;2020&#x2F;12&#x2F;201215131236.htm&quot;&gt;наш мозок сприймає код як головоломку&lt;&#x2F;a&gt;, тож ми можемо помилково ігнорувати наслідки змін — головоломку ж вирішено, кінець, з нею більше нічого не станеться… Тому має сенс розповідати історії і підчас написання PR. І у тебе є багато можливостей для цього&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-02-7n59SFM6l8:more-of-a-suggestion&quot; id&#x3D;&quot;rfn:post-2021-02-7n59SFM6l8:more-of-a-suggestion&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;words-mean-things&quot;&gt;Words Mean Things&lt;&#x2F;h2&gt;
&lt;p&gt;Оповідання можна почати задовго до натискання «New pull request» — у самому коді. Навіть без коментарів, ти можеш описати «основних діючих осіб» у &lt;strong&gt;іменах файлів, функцій та змінних&lt;&#x2F;strong&gt;. Гарне імʼя сутності опише, що і навіщо вона, покаже звʼязок з іншим кодом, і допоможе з її обговоренням. Тому важливо уникати загальних назв (на кшталт &lt;code&gt;utils&#x2F;data.js&lt;&#x2F;code&gt;) та спільних імен для позначення різних речей, і звертати увагу на існуючі імена — якщо фіча вже називається &lt;code&gt;wunderwaffle&lt;&#x2F;code&gt;, то не починай раптово називати її &lt;code&gt;terrifictiramisu&lt;&#x2F;code&gt;. Звичайно, якщо зрозуміла, що файлу більше підходить інша назва, то, хоча ґіт і розумний, зазвичай краще явно зробити &lt;code&gt;git mv A B&lt;&#x2F;code&gt; — так зберігається історія і простіше створювати коміти&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Імʼя гілки&lt;&#x2F;strong&gt; допоможе сфокусувати PR і не відволікати себе та ревʼюверів на другорядні «сюжети». Таким чином, у гілці &lt;code&gt;JD-48&#x2F;speed-up-uploads&lt;&#x2F;code&gt;, можна спіймати і зупинити себе від оновлення усіх залежностей у проєкті JD або від змін у завантаженні файлі у проєкті ER&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;the-medium-is-the-commit-message&quot;&gt;The Medium is the (Commit) Message&lt;&#x2F;h2&gt;
&lt;p&gt;Якщо ж тебе все-таки занесло і у гілці вже купа змін, які не повʼязані з основною метою PR’у, але вони будуть корисні для проєкту та&#x2F;або іншого проєкта, то можна погратися зі &lt;strong&gt;змістом комітів&lt;&#x2F;strong&gt;. Замість коміту з усіма змінами у файлі, нічого не заважає додати ті зміни рядково. Поціновувачі CLI можуть зробити це з &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-add#Documentation&#x2F;git-add.txt---patch&quot;&gt;&lt;code&gt;git add --patch&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; або &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-add#Documentation&#x2F;git-add.txt---interactive&quot;&gt;&lt;code&gt;git add --interactive&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, я же звик до інтерфейсу &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.git-tower.com&#x2F;&quot;&gt;Tower’у&lt;&#x2F;a&gt; для цього&lt;&#x2F;p&gt;
&lt;p&gt;Як зрозуміти, які рядки доречні у одному коміті, а які — у іншому? Подумай, де ці рядки опинилися би у «сюжеті» PR’у, і опиши це у &lt;strong&gt;імені коміту&lt;&#x2F;strong&gt;, обмежившись &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;tbaggery.com&#x2F;2008&#x2F;04&#x2F;19&#x2F;a-note-about-git-commit-messages.html&quot;&gt;50 символами, які рекомендує git&lt;&#x2F;a&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-02-7n59SFM6l8:50-plus&quot; id&#x3D;&quot;rfn:post-2021-02-7n59SFM6l8:50-plus&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. Можливо, ця історія — короткий анекдот, і &lt;code&gt;fix A when B&lt;&#x2F;code&gt; буде достатньо. Можливо, у «сюжеті» PR’у декілька персонажів, з якими спочатку треба познайомити читача (&lt;code&gt;add X to do Y&lt;&#x2F;code&gt;), а потім вже один до одного (&lt;code&gt;use X in Z&lt;&#x2F;code&gt;) &lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Describe your changes in imperative mood, e.g. «make xyzzy do frotz» instead of «[This patch] makes xyzzy do frotz» or «[I] changed xyzzy to do frotz», as if you are giving orders to the codebase to change its behavior.  Try to make sure your explanation can be understood without external resources. Instead of giving a URL to a mailing list archive, summarize the relevant points of the discussion.&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git.kernel.org&#x2F;pub&#x2F;scm&#x2F;git&#x2F;git.git&#x2F;tree&#x2F;Documentation&#x2F;SubmittingPatches?id&#x3D;HEAD#n136&quot;&gt;SubmittingPatches - The core git plumbing&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Також, у багатьох проєктах налаштована інтеграція між Git та Jira, для того щоб підвʼязувати коміти та&#x2F;або гілки до тікетів. Тому часто має сенс згадати номер тікету у імені коміту, наприклад &lt;code&gt;#JD-48 cache responses from X&lt;&#x2F;code&gt;. Формат (&lt;code&gt;XX-00&lt;&#x2F;code&gt;, &lt;code&gt;$XX-00&lt;&#x2F;code&gt;, &lt;code&gt;#XX-00&lt;&#x2F;code&gt;) відрізняється від проекту до проекту, але тому ж Github’у &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github&#x2F;administering-a-repository&#x2F;configuring-autolinks-to-reference-external-resources&quot;&gt;достатньо будь-якого префіксу перед цифрами&lt;&#x2F;a&gt; для того, щоб відобразити номери тікетів як посилання на них&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;fIAjachuzUKWXol3Uc6L5KCeo4.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ще, багато хто є послідовниками &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.conventionalcommits.org&#x2F;&quot;&gt;conventional commit&lt;&#x2F;a&gt; і додають теги на кшталт &lt;code&gt;feat&lt;&#x2F;code&gt; та &lt;code&gt;chore&lt;&#x2F;code&gt;. Мені ці теги здаються марними для продукту без публічного CHANGELOG’у, але звичка категоризувати зміни може допомогти з фокусуванням PR’ів та комітів&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;retelling-the-story&quot;&gt;Retelling the Story&lt;&#x2F;h2&gt;
&lt;p&gt;Якщо наплутав з &lt;strong&gt;порядком комітів&lt;&#x2F;strong&gt;, то його можна виправити за допомогою &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-rebase#_interactive_mode&quot;&gt;&lt;code&gt;git rebase --interactive&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, з яким можна легко переставити&#x2F;переписати&#x2F;обʼєднати&#x2F;взагалі викинути коміти, редагуючи текстовий файл зі списком операцій та комітів&lt;&#x2F;p&gt;
&lt;p&gt;Наприклад, ми працювали у гілці &lt;code&gt;JD-48&lt;&#x2F;code&gt;, де написали пʼять комітів. Локальна історія комітів зараз виглядає якось так (&lt;code&gt;git log --graph --oneline --all&lt;&#x2F;code&gt;, нові коміти зверху) :&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-git&quot;&gt;* f4593f9 (HEAD -&amp;gt; JD-48) add tests for X-Y integration
* 04d0fda optimize Y
* ba46169 integrate X with Y
* fa1afe1 tests for X
* 5928aea setup X
* 300a500 (master, origin&#x2F;master, origin&#x2F;HEAD) …
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;git rebase --interactive &quot;5928aea^&quot;&lt;&#x2F;code&gt; відкриє у текстовому редакторі&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-02-7n59SFM6l8:vi&quot; id&#x3D;&quot;rfn:post-2021-02-7n59SFM6l8:vi&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; файл з такими рядками:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;5928&lt;&#x2F;span&gt;aea setup &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick fa1afe&lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt; tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick ba&lt;span class&#x3D;&quot;hljs-number&quot;&gt;46169&lt;&#x2F;span&gt; integrate &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt; with &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;04&lt;&#x2F;span&gt;d0fda optimize &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick f4593f&lt;span class&#x3D;&quot;hljs-number&quot;&gt;9&lt;&#x2F;span&gt; add tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;-&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt; integration
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Кожен рядок — це операція, яку треба виконати підчас створення нової версії гілки. За замовчуванням, rebase бере і застосовує кожен коміт (&lt;code&gt;pick&lt;&#x2F;code&gt;). Крім цього, коміт можна пропустити (&lt;code&gt;drop&lt;&#x2F;code&gt;), обʼєднати з попереднім (&lt;code&gt;squash&lt;&#x2F;code&gt;), застосувати-з-можливістю редагування (&lt;code&gt;fixup&lt;&#x2F;code&gt;); також можна змінити порядок комітів (перестановкою рядків) або виконати довільні shell-команди (&lt;code&gt;exec&lt;&#x2F;code&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;Наприклад, якщо ми хочемо:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;перенести коміт з тестами (&lt;code&gt;fa1afe1&lt;&#x2F;code&gt;) в кінець гілки;&lt;&#x2F;li&gt;
&lt;li&gt;обʼєднати його з тестами інтеграції (&lt;code&gt;f4593f9&lt;&#x2F;code&gt;);&lt;&#x2F;li&gt;
&lt;li&gt;і викинути коміт про оптимізацію іншої частини проєкту (тому що PR про нову фічу, а не про оптимізацію)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;то треба відредагувати «сценарій», який згенерував &lt;code&gt;git rebase --interactive&lt;&#x2F;code&gt;, до такого:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pick &lt;span class&#x3D;&quot;hljs-number&quot;&gt;5928&lt;&#x2F;span&gt;aea setup &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
pick ba&lt;span class&#x3D;&quot;hljs-number&quot;&gt;46169&lt;&#x2F;span&gt; integrate &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt; with &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
drop &lt;span class&#x3D;&quot;hljs-number&quot;&gt;04&lt;&#x2F;span&gt;d0fda optimize &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt;
pick fa1afe&lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt; tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;
squash f4593f&lt;span class&#x3D;&quot;hljs-number&quot;&gt;9&lt;&#x2F;span&gt; add tests for &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;X&lt;&#x2F;span&gt;-&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;Y&lt;&#x2F;span&gt; integration
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Або можна не хизуватися і переписувати історію за допомогою мишки у GUI, наприклад у вже згаданому &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.git-tower.com&#x2F;help&#x2F;guides&#x2F;commit-history&#x2F;interactive-rebase&#x2F;mac&quot;&gt;Tower’і&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Після того, як гілка та коміти відполіровані, можна взятися за &lt;strong&gt;імʼя та опис PR’у&lt;&#x2F;strong&gt;. Вони, здебільшого. пишуться аналогічно до комітів, але більш високорівнево&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;що-писати-у-описі-pull-requestу&quot;&gt;Що писати у описі pull request’у?&lt;&#x2F;h2&gt;
&lt;p&gt;Зазвичай, для мене найскладніше у описі PR’у — це почати. Чистий  &lt;del&gt;лист&lt;&#x2F;del&gt; &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; багатьох блокує&lt;&#x2F;p&gt;
&lt;p&gt;В деяких репозиторіях є &lt;code&gt;PULL_REQUEST_TEMPLATE.md&lt;&#x2F;code&gt;, який може допомогти з цим, але у приватних проєктах часто або його нема, або він доволі безпорадний для надання контексту:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;&lt;span class&#x3D;&quot;hljs-section&quot;&gt;# Summary&lt;&#x2F;span&gt;
...

---

&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; [ ] &amp;lt;!-- some-continuous-integration-checkbox --&amp;gt; ci-bot, do something
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Також, з порожнім &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; можуть допомогти імена та описи комітів. Якщо ж їх недостатньо, то почни з «Коротше, є &lt;code&gt;іменник&lt;&#x2F;code&gt; і воно &lt;code&gt;дієслово&lt;&#x2F;code&gt;» і обвішай цей вступ контекстом&lt;&#x2F;p&gt;
&lt;p&gt;Цим контекстом можуть бути:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;причини для змін (тільки не «мені менеджер сказала», а «у користувачів проблема»)&lt;&#x2F;li&gt;
&lt;li&gt;дивні місця, де &lt;em&gt;здається&lt;&#x2F;em&gt; що можна краще, але ліньки або не знаєш як. Можливо ті дивності не є проблемою, можливо хтось знає гарне рішення, можливо хтось вмотивує здолати лінь&lt;&#x2F;li&gt;
&lt;li&gt;посилання на повʼязані тікети та PR’и; на документацію нових&#x2F;дивних моментів; на відповіді з StackOverflow, з яких ти скопіював той сніпет…&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Доречі про посилання. Десять хвилин на вивчення Markdown’у &lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-02-7n59SFM6l8:markdown-flavors&quot; id&#x3D;&quot;rfn:post-2021-02-7n59SFM6l8:markdown-flavors&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; допоможуть з тим, що ці посилання а) не заважали читати і писати опис; б) знаходились у правильному місці&lt;&#x2F;p&gt;
&lt;p&gt;Наприклад, порівняй читабельність тексту з різними стилями посилань:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color&lt;&#x2F;span&gt;),
but we [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;can’t use it&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color&lt;&#x2F;span&gt;) yet in all of
[&lt;span class&#x3D;&quot;hljs-string&quot;&gt;our supported browsers&lt;&#x2F;span&gt;](&lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers&lt;&#x2F;span&gt;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s &lt;span class&#x3D;&quot;hljs-code&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;, but we can’t use it yet in all of our supported browsers

Links:
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color
&lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-markdown&quot;&gt;There’s [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;input type&#x3D;&quot;color&quot;&amp;gt;&#x60;&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;mdn&lt;&#x2F;span&gt;], but we [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;can’t use it&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;caniuse&lt;&#x2F;span&gt;] yet in all of [&lt;span class&#x3D;&quot;hljs-string&quot;&gt;our supported browsers&lt;&#x2F;span&gt;][&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;kb&lt;&#x2F;span&gt;]

[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;mdn&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTML&#x2F;Element&#x2F;input&#x2F;color&lt;&#x2F;span&gt;
[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;caniuse&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;caniuse.com&#x2F;#feat&#x3D;input-color&lt;&#x2F;span&gt;
[&lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;kb&lt;&#x2F;span&gt;]: &lt;span class&#x3D;&quot;hljs-link&quot;&gt;https:&#x2F;&#x2F;kb.example.com&#x2F;supported-browsers&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;передача-естафети&quot;&gt;Передача естафети&lt;&#x2F;h2&gt;
&lt;p&gt;Після того, як уся проза написана, їй потрібна авдиторія. Додай в assignee зацікавлених колег (будь то окремі люди або цілі команди) і, закінчивши з текстом на Github’і, можна витратити хвилинку на «анонс» pull request’у у робочому чаті і видихнути, поки колеги читають, вивчають, та коментують&lt;&#x2F;p&gt;
&lt;p&gt;Перегляд PR’у заслуговує окремого запису, до якого я повернусь після того, як поділюсь &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-assignee.html&quot;&gt;парою думок про assignee&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-02-7n59SFM6l8:more-of-a-suggestion&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Ще раз підкреслю, що далі будуть &lt;em&gt;можливості&lt;&#x2F;em&gt; для покращення pull request’у, а не &lt;em&gt;вимоги&lt;&#x2F;em&gt; до нього. Якщо раніше обходився особистим обговоренням і парою слів у назві, то ніхто не змушує тебе витрачати години на написання епопеї про зміну трьох рядків 🙏&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-02-7n59SFM6l8:more-of-a-suggestion&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-02-7n59SFM6l8:50-plus&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Коли одного рядка у 50 символів недостатньо, імʼя коміту можна і треба доповнювати додатковим описом (або додавши перенос рядка, якщо працюєш через &lt;code&gt;git&lt;&#x2F;code&gt; CLI, або використавши окреме тестове поле у GUI), будь воно написаним з нуля або згенерованим з &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;thoughtbot.com&#x2F;blog&#x2F;better-commit-messages-with-a-gitmessage-template&quot;&gt;шаблону&lt;&#x2F;a&gt; &lt;em&gt;(дякую &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;markbaraban&#x2F;status&#x2F;1363793616074928129&quot;&gt;Марку&lt;&#x2F;a&gt; за пораду)&lt;&#x2F;em&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-02-7n59SFM6l8:50-plus&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-02-7n59SFM6l8:vi&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Якщо не подобається &lt;code&gt;vi&lt;&#x2F;code&gt;, то &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;unix.stackexchange.com&#x2F;questions&#x2F;73484&#x2F;how-can-i-set-vi-as-my-default-editor-in-unix&quot;&gt;зміни &lt;code&gt;$EDITOR&lt;&#x2F;code&gt; у своєму &lt;code&gt;~&#x2F;.bashrc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;~&#x2F;.zshrc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;~&#x2F;.config&#x2F;fish&#x2F;config.fish&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; щоб запускати, наприклад, Sublime Text (&lt;code&gt;subl -w&lt;&#x2F;code&gt;) або будь-який інший текстовий редактор&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-02-7n59SFM6l8:vi&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2021-02-7n59SFM6l8:markdown-flavors&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Будь то «оригінальний» &lt;a href&#x3D;&quot;http:&#x2F;&#x2F;daringfireball.net&#x2F;projects&#x2F;markdown&#x2F;&quot;&gt;варіант Груберу&lt;&#x2F;a&gt;, більш деталізовані &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;guides.github.com&#x2F;features&#x2F;mastering-markdown&#x2F;&quot;&gt;GitHub Flavored Markdown&lt;&#x2F;a&gt;&#x2F;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;commonmark.org&#x2F;help&#x2F;&quot;&gt;CommonMark&lt;&#x2F;a&gt;, або взагалі інша мова розмітки, яку використовує твій інструмент для code review&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-02-7n59SFM6l8:markdown-flavors&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/code-review-story.html</link>
      <guid isPermaLink="false">post-2021-02-7n59SFM6l8</guid>
      <pubDate>Sun, 21 Feb 2021 15:45:00 GMT</pubDate>
    </item>
    <item>
      <title>Навіщо code review</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;dQmCL9NHbmILsnznv7Y4jAs4YD.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Можливо, я один такий, але мене дратують жарти типу&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;blockquote cite&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;
        10 lines of code &#x3D; 10 issues.&lt;br&gt;&lt;br&gt;500 lines of code &#x3D; &quot;looks fine.&quot;&lt;br&gt;&lt;br&gt;Code reviews.
      &lt;&#x2F;blockquote&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;iamdevloper&#x2F;status&#x2F;397664295875805184&quot;&gt;&lt;b&gt;I Am Devloper on Twitter&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ну, тобто сам жарт ок — &lt;code&gt;i-know-that-feel.png&lt;&#x2F;code&gt;, &lt;code&gt;#relatable&lt;&#x2F;code&gt;, усе таке. Але у ньому кілька прикладів поганої поведінки і авторів коду, і ревʼюверів:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;поверхневі коментарі;&lt;&#x2F;li&gt;
&lt;li&gt;величезні патчі;&lt;&#x2F;li&gt;
&lt;li&gt;нестача контексту;&lt;&#x2F;li&gt;
&lt;li&gt;читання «по діагоналі»&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Тож якщо проблеми з code review &lt;em&gt;настільки&lt;&#x2F;em&gt; поширені, то на них треба не лише показувати пальцем, але і якось виправляти. Зробити це можна або зовнішними вимогами&#x2F;обмеженнями, або внутрішньою мотивацією&lt;&#x2F;p&gt;
&lt;p&gt;Вимагати «будь краще» — глупо. Автоматично відсікати code review через метрики — не варіант, тому що «зрозумілість» не виміряти скріптом. Тому «покращити code review зовнішніми вимогами&#x2F;обмеженнями» відпадає&lt;&#x2F;p&gt;
&lt;p&gt;Вплинути на внутрішню мотивацію можна особистим прикладом, але після років спроб, це здається занадто повільним і не те щоб надійним. Можливо, особистий приклад буде швидше донести словами&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Серія записів про code review:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-why.html&quot;&gt;Навіщо?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;Створення pull request’у&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-assignee.html&quot;&gt;Думки про pull request assignee&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-response.html&quot;&gt;Code review &amp;amp; response&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Англійською: &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;on-code-reviews.html&quot;&gt;On Code Reviews&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;h2 id&#x3D;&quot;час-та-місце&quot;&gt;Час та місце&lt;&#x2F;h2&gt;
&lt;p&gt;Я &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;anton.codes&#x2F;#jobs&quot;&gt;працював&lt;&#x2F;a&gt; лише у доволі зрілих продуктових компаніях. Коли компанія роками працює над продуктом, внесок часу та зусиль у code review виправдано не лише через «ну, це правильно», але і через спрощення підтримки у довгому строку&lt;&#x2F;p&gt;
&lt;p&gt;Зовсім молоді продукти, для яких того самого довгого строку може ніколи і не бути, ризикують тим, що після усіх review залишиться лише «правильний», але нікому не потрібний код&lt;&#x2F;p&gt;
&lt;p&gt;Як виглядають справи з клієнтською роботою (агенції&#x2F;аутсорс&#x2F;аутстаф) я не знаю. З одного боку, якщо клієнт повернеться за доопрацюванням через рік-другий, минулі code review допоможуть швидше згадати&#x2F;впізнати кодову базу. З іншого — клієнт може ніколи не повернутися&lt;&#x2F;p&gt;
&lt;p&gt;Тож code review може бути зовсім марним (або взагалі шкідливим) на твоєму місці роботи. Або він був таким на минулому місці роботи твоєї нової колеги…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;мотивація&quot;&gt;Мотивація&lt;&#x2F;h2&gt;
&lt;p&gt;Окей, припустимо ти працюєш у відповідній команді, у відповідний час, і тобі дуже хочеться краще писати та коментувати pull request’и. Щоб знати напрямок для покращень, треба розуміти, яка мета code review&lt;&#x2F;p&gt;
&lt;p&gt;Тож, навіщо проводити code review?&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Мені сказали, що тут так прийнято&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;«Так історично склалося» — поганий шлях до покращень. Він приведе туди ж, звідки він почався. Навіть нема буде про що розповісти (якщо тобі не подобаються історії, в яких нічого не відбувається)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Щоб ідіоти нічого не поламали у моєму ідеальному проєкті&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Ці «ідіоти» відкрили PR тому що їм треба щось виправити або щось покращити. Якщо твій ідеальний проєкт не тримається на плаву лише завдяки твоєму gatekeeping’у, то краще спрямуй зусилля в написання тестів і &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;reply-about-linters.html&quot;&gt;налаштування&lt;&#x2F;a&gt; лінтеру &lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Щоб зловити те, що важко або взагалі неможливо покрити тестами&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Тепліше… Але «володарям знань» швидко набридне вдивлятися сотні та тисячі рядків коду у пошуках одних і тих самих проблем, з якими стикаються колеги, незнайомі з репозиторієм&lt;&#x2F;p&gt;
&lt;p&gt;З «зробити собі побільше рутини» виходить погана мета. Хотілося б, щоб &lt;em&gt;воно саме&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Але новачки репозиторію (саме репозиторію — в них може бути величезний досвід розробки взагалі) не знають, що &lt;code&gt;{placeholder}&lt;&#x2F;code&gt;створить проблеми. А досвідчені вже настільки часто стикалися з &lt;code&gt;{placeholder}&lt;&#x2F;code&gt;, що інстинктивно його уникають&lt;&#x2F;p&gt;
&lt;p&gt;&lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;EfD2pNDO8InhgJg7tNqMhl2Pgn&#x2F;gifv.mp4&quot; autoplay&#x3D;&quot;&quot; muted&#x3D;&quot;&quot; loop&#x3D;&quot;&quot; disableremoteplayback&#x3D;&quot;&quot;&gt;&lt;&#x2F;video&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Тому, щоб &lt;em&gt;воно саме&lt;&#x2F;em&gt;, автори та ревʼювери мають обмінюватися особистим розумінням проєкту:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;автори явно та неявно вказують на дивину у існуючому коді. Наприклад, що для досягнення простої мети треба зробити купу ручної роботи&lt;&#x2F;li&gt;
&lt;li&gt;ревʼювери діляться базовими припущеннями, на яких працює проєкт, на кшталт «на помилки ми повертаємо код 200 и обʼєкт с ключем &lt;code&gt;error&lt;&#x2F;code&gt; через рішення, яке було прийняте десять років тому и навколо якого написано &lt;em&gt;багато&lt;&#x2F;em&gt; коду»&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Іншими словами, you need two to &lt;del&gt;tango&lt;&#x2F;del&gt; have a good code review&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Продовження: &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;code-review-story.html&quot;&gt;Створення pull request’у&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;

        
      </description>
      <link>https://zemlan.in/code-review-why.html</link>
      <guid isPermaLink="false">post-2021-02-CSuMEA2cQt</guid>
      <pubDate>Mon, 15 Feb 2021 10:10:00 GMT</pubDate>
    </item>
    <item>
      <title>iOS Lock Screen, Powered by Microsoft</title>
      <description>
        &lt;p&gt;&lt;em&gt;Где компромиссное решение в сто строк кода заменяется бескомпромиссным в пять прямоугольников со скруглёнными углами&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Первым виджетом, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;widgetarian-wmuwme.html&quot;&gt;который я набросал&lt;&#x2F;a&gt; в &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;scriptable.app&quot;&gt;Scriptable&lt;&#x2F;a&gt;, был &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;zemlanin&#x2F;status&#x2F;1317458198153015303&quot;&gt;список приложений с виндовой фотографией дня&lt;&#x2F;a&gt; (она же — &quot;фотография на фоне bing.com&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;Мне нравится &quot;свежесть&quot; экрана блокировки Windows и хотелось иметь что-то подобное и на телефоне. В бетах iOS 13 во &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;ru-ru&#x2F;guide&#x2F;shortcuts&#x2F;welcome&#x2F;ios&quot;&gt;встроенном приложении Shortcuts (&quot;Команды&quot;)&lt;&#x2F;a&gt; даже было действие для установки произвольной картинки изображением на домашний экран и&#x2F;или экран блокировки. Но &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;shortcuts&#x2F;comments&#x2F;cnbhjq&#x2F;set_wallpaper_removed_in_ios_13_beta_6&#x2F;&quot;&gt;оно не дожило до релиза iOS 13&lt;&#x2F;a&gt; и не вернулось к релизу iOS 14&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;компромисс&quot;&gt;Компромисс&lt;&#x2F;h2&gt;
&lt;p&gt;Так что единственной возможностью иметь что-то &lt;em&gt;близкое&lt;&#x2F;em&gt; к динамической обоине из интернета было сделать обновляющийся виджет. Ссылку на волшебный JSON со ссылкой на фотографию дня нашёл, само собой, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;a&#x2F;18096210&quot;&gt;на Stack Overflow&lt;&#x2F;a&gt;. Список ссылок&#x2F;приложений поверх картинки добавил чуть позже, как чтобы домашний экран не терял своё главное назначение — запуск этих приложений&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;e5xyxdMNeUFj4J8RVBAU0cSBlB.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Спустя ещё некоторое время, добавил кэширование фотографии дня, чтобы телефон не загружал её каждый раз, когда iOS решала обновить виджет. Я не сохранял промежуточные версии скрипта, но сейчас он выглядит как-то так (&lt;em&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;ios-bing-wallpaper.html#%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D0%BB-%D1%87%D1%82%D0%BE-%D1%85%D0%BE%D1%82%D0%B5%D0%BB&quot;&gt;пропустить простынку кода и перейти к более интересной части&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;APPS&lt;&#x2F;span&gt; &#x3D; [
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;photos-redirect:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;name&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;photos&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;App-prefs:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;name&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;settings&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;separator&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;true&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;bear:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;things:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;reeder:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;twitterrific:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;separator&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;true&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;spotify:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;overcast:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt; },
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; { url: &quot;music:&#x2F;&#x2F;&quot; },&lt;&#x2F;span&gt;
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;separator&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;true&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;onepassword:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;name&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;1password&quot;&lt;&#x2F;span&gt; },
  { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;url&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;otpauth:&#x2F;&#x2F;&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;name&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;authenticator&quot;&lt;&#x2F;span&gt; },
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; { url: &quot;diia.app:&#x2F;&#x2F;&quot;, name: &quot;diia&quot; },&lt;&#x2F;span&gt;
];

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;NOW&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widget &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ListWidget&lt;&#x2F;span&gt;();
widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);
widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;black&lt;&#x2F;span&gt;();
&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; refresh every 4 hours&lt;&#x2F;span&gt;
widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;refreshAfterDate&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;(+&lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;NOW&lt;&#x2F;span&gt; + &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1000&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;formatDate&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;date, format&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; formatter &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;DateFormatter&lt;&#x2F;span&gt;()
  formatter.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;dateFormat&lt;&#x2F;span&gt; &#x3D; format
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; formatter.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;string&lt;&#x2F;span&gt;(date)
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;async&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getTodaysImage&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; fm &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;FileManager&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;local&lt;&#x2F;span&gt;()
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; imagesDirectory &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${fm.cacheDirectory()}&lt;&#x2F;span&gt;&#x2F;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${Script.name()}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (!fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;fileExists&lt;&#x2F;span&gt;(imagesDirectory)) {
    fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createDirectory&lt;&#x2F;span&gt;(imagesDirectory, &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;true&lt;&#x2F;span&gt;)
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; startdate &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;formatDate&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;NOW&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;YYYYMMdd&quot;&lt;&#x2F;span&gt;)
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; metadataPath &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${imagesDirectory}&lt;&#x2F;span&gt;&#x2F;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${startdate}&lt;&#x2F;span&gt;.json&#x60;&lt;&#x2F;span&gt;
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;let&lt;&#x2F;span&gt; todaysImage
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;fileExists&lt;&#x2F;span&gt;(metadataPath)) {
    todaysImage &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;JSON&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;parse&lt;&#x2F;span&gt;(fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;readString&lt;&#x2F;span&gt;(metadataPath))
  } &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;else&lt;&#x2F;span&gt; {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; bingReq &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Request&lt;&#x2F;span&gt;(
      &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;https:&#x2F;&#x2F;www.bing.com&#x2F;HPImageArchive.aspx?format&#x3D;js&amp;amp;idx&#x3D;0&amp;amp;n&#x3D;1&quot;&lt;&#x2F;span&gt;
    );
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { images } &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; bingReq.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;loadJSON&lt;&#x2F;span&gt;();
    todaysImage &#x3D; images[&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;];
    
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (startdate &#x3D;&#x3D;&#x3D; todaysImage.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;startdate&lt;&#x2F;span&gt;) {
      fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeString&lt;&#x2F;span&gt;(metadataPath, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;JSON&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;stringify&lt;&#x2F;span&gt;(todaysImage))
    }
  }
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; imagePath &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${imagesDirectory}&lt;&#x2F;span&gt;&#x2F;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${todaysImage.startdate}&lt;&#x2F;span&gt;.jpg&#x60;&lt;&#x2F;span&gt;
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;fileExists&lt;&#x2F;span&gt;(imagePath)) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; {
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;image&lt;&#x2F;span&gt;: fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;readImage&lt;&#x2F;span&gt;(imagePath),
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;copyrightlink&lt;&#x2F;span&gt;: todaysImage.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;copyrightlink&lt;&#x2F;span&gt;
    }
  }
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; image &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Request&lt;&#x2F;span&gt;(
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;https:&#x2F;&#x2F;www.bing.com&quot;&lt;&#x2F;span&gt; + todaysImage.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;url&lt;&#x2F;span&gt;
  ).&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;loadImage&lt;&#x2F;span&gt;()
  
  fm.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;writeImage&lt;&#x2F;span&gt;(imagePath, image)

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; {
    image,
    &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;copyrightlink&lt;&#x2F;span&gt;: todaysImage.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;copyrightlink&lt;&#x2F;span&gt;,
  };
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { image, copyrightlink } &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;await&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;getTodaysImage&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundImage&lt;&#x2F;span&gt; &#x3D; image;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; row &#x3D; widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();

row.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutHorizontally&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; copyrightColumn &#x3D; row.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
copyrightColumn.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutVertically&lt;&#x2F;span&gt;();
copyrightColumn.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
copyrightColumn.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; copyrightSymbol &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;SFSymbol&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;named&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;c.circle&quot;&lt;&#x2F;span&gt;);
copyrightSymbol.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;applyThinWeight&lt;&#x2F;span&gt;();
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; c &#x3D; copyrightColumn.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addImage&lt;&#x2F;span&gt;(copyrightSymbol.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;image&lt;&#x2F;span&gt;);
c.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;url&lt;&#x2F;span&gt; &#x3D; copyrightlink;
c.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;resizable&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;false&lt;&#x2F;span&gt;;
c.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;tintColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();

row.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; column &#x3D; row.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
column.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutVertically&lt;&#x2F;span&gt;();
column.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;14&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;12&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;for&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { separator, url, name } &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;of&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;APPS&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (separator) {
    column.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;continue&lt;&#x2F;span&gt;;
  }
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; appRow &#x3D; column.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
  appRow.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
  appRow.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;2&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;16&lt;&#x2F;span&gt;);

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; t &#x3D; appRow.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addText&lt;&#x2F;span&gt;(name || url.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;replace&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-regexp&quot;&gt;&#x2F;:\&#x2F;\&#x2F;$&#x2F;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&lt;&#x2F;span&gt;));
  t.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;rightAlignText&lt;&#x2F;span&gt;();
  t.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();
  t.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;shadowColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;black&lt;&#x2F;span&gt;();
  t.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;shadowRadius&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;;
  t.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;font&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Font&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Menlo&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;18&lt;&#x2F;span&gt;);

  appRow.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;url&lt;&#x2F;span&gt; &#x3D; url;
}

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;presentLarge&lt;&#x2F;span&gt;();
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;получил-что-хотел&quot;&gt;Получил, что хотел&lt;&#x2F;h2&gt;
&lt;p&gt;К счастью, в iOS 14.3 в Shortcuts вернулось действие &quot;Установить X в качестве обоев&quot;, так что &quot;динамический экран блокировки&quot; стал возможен без необходимости в компромиссах. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.icloud.com&#x2F;shortcuts&#x2F;bcc7c9ebb1634777bc9a1ec7515cc49b&quot;&gt;Этот shortcut&lt;&#x2F;a&gt; делает необходимый минимум и ничего больше:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Постучаться по ссылке &lt;code&gt;https:&#x2F;&#x2F;www.bing.com&#x2F;HPImageArchive.aspx?format&#x3D;js&amp;amp;idx&#x3D;0&amp;amp;n&#x3D;1&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Получить из ответа адрес сегодняшней фотографии. Shortcuts считает по-человечески, начиная с единицы, так что &quot;&lt;code&gt;url&lt;&#x2F;code&gt; первого элемента списка &lt;code&gt;images&lt;&#x2F;code&gt;&quot; доступно по ключу &lt;code&gt;images.1.url&lt;&#x2F;code&gt;, без программистских &quot;мы считаем с нуля потому что наши деды считали с нуля&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Постучаться по ссылке из прошлого шага, дописав к ней &lt;code&gt;https:&#x2F;&#x2F;bing.com&#x2F;&lt;&#x2F;code&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2021-02-8zwfNzYZ0b:slash&quot; id&#x3D;&quot;rfn:post-2021-02-8zwfNzYZ0b:slash&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. Для комбинирования фиксированного текста с результатами предыдущего действия есть &lt;em&gt;специальное&lt;&#x2F;em&gt; действие, но можно &quot;срезать угол&quot; и сделать это с помощью &quot;волшебной переменной&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Собственно, установить новую обоину. Чтобы телефон не просил никаких подтверждений и тупо использовал центр фотографии, я выключил опцию &quot;Показать окно просмотра&quot;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;jK28fUBWkUO3vUw9FOaFVdih7e.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;fN6YEWLfZlXuk1pyF2t4sZeDP6.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;8vzLenWjQFOO7qIQCxxHqPqJEb.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;2eujwJdhYYxaCqlTJWXLKUmMbB.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Чтобы автоматически запускать эту команду каждый день (например, когда заходит солнце), в Shortcuts есть вкладка &quot;Автоматизация&quot;:&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;K1XxXdhWoBihOaJkmWMCYEKmZL.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;z3Yfv2iEHRIGKd1T6GbnQrowEi.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;YL3ncOS83sChj0EW3ytldrNgbO.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;И готово! Теперь каждый вечер мой экран блокировки обновляется красивой фотографией ☺️&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;82aJrg3yepOJL7vAhYlc4Aa2fd.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;1RfsqxsjMLvCGVTDxqUZbMG8Vn.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;ZUL9dRbRFv8ITOzuab6MiYUSut.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Ls4lx8XhovpdAIK0q1tK7z1CRA.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;gr5ZmigRXsS31zJlJ2MwmHwJYG.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;cygpi7JFfKNk0XSCxC1ZSAasFs.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;ссылки&quot;&gt;Ссылки&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;scriptable.app&quot;&gt;Scriptable&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;ru-ru&#x2F;guide&#x2F;shortcuts&#x2F;welcome&#x2F;ios&quot;&gt;Руководство пользователя Быстрых команд&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id&#x3D;&quot;ps--2021-05-02&quot;&gt;P.S. &#x2F;&#x2F; 2021-05-02&lt;&#x2F;h3&gt;
&lt;p&gt;Иногда, автоматический кроппинг порождает шедевры:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;e8com2NKPJFlmfFg9QkhQ39Pou&#x2F;fit1000.jpeg&quot; alt&#x3D;&quot;&quot; width&#x3D;&quot;462&quot; height&#x3D;&quot;1000&quot;&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2021-02-8zwfNzYZ0b:slash&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;code&gt;&#x2F;&lt;&#x2F;code&gt; в конце необязательный, но с ним скриншот читается немного получше&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2021-02-8zwfNzYZ0b:slash&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/ios-bing-wallpaper.html</link>
      <guid isPermaLink="false">post-2021-02-8zwfNzYZ0b</guid>
      <pubDate>Sat, 06 Feb 2021 10:48:00 GMT</pubDate>
    </item>
    <item>
      <title>Widgetarian</title>
      <description>
        &lt;p&gt;&lt;em&gt;Если бы март в этом году не затянулся, то я скорее всего скатался бы на BeerJSSummit 2020 с «сиквелом» &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;awesome-demos-done-quick.html&quot;&gt;выступления про всякую автоматизацию&lt;&#x2F;a&gt;, но на iOS&#x2F;iPadOS вместо macOS. Но произошло то, что произошло, рассказать про JS на iOS&#x2F;iPadOS всё ещё хочется, так что here we go…&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;В iOS, в отличии от macOS, нет JXA. Но у нативных приложений уже довольно давно есть доступ к &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nshipster.com&#x2F;javascriptcore&#x2F;&quot;&gt;JavaScriptCore&lt;&#x2F;a&gt; для запуска JS-кода. Окружение этого кода отличается от браузерного или &lt;code&gt;nodejs&lt;&#x2F;code&gt;-ового, но приложение может определить свои собственные глобальные функции и объекты, при взаимодействии с которыми будут дёргаться нативные API. Зачастую, про JavaScriptCore вспоминают в контексте React Native, но нам это &lt;del&gt;сегодня&lt;&#x2F;del&gt; не интересно&lt;&#x2F;p&gt;
&lt;p&gt;Кроме приложений, которые используют JS чтобы рисовать кривые интерфейсы, есть приложения вроде &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;scriptable.app&quot;&gt;Scriptable&lt;&#x2F;a&gt;, в которых можно запускать произвольный JS-код с доступом к GPS, к HTTP, к локальным фото&#x2F;календарю&#x2F;контактам, файлам в iCloud…&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Пфф, Shortcuts умеет всё это и даже больше! Зачем приплетать сюда JS?  &lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;А затем, что в iOS 14 завезли виджеты…&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
      &lt;video playsinline&#x3D;&quot;&quot; src&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;tweets_media&#x2F;1317458198153015303-nVJRMjXeu1ZxH1Y4.mp4&quot; controls&#x3D;&quot;&quot; preload&#x3D;&quot;none&quot; poster&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;oLhH7G1qRNtUy9XNFntaMrpMK6&#x2F;firstframe.jpeg&quot;&gt;&lt;&#x2F;video&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1317458198153015303&quot;&gt;&lt;b&gt;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i&gt;
              A E S T E T I C S • J S
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1322221511776886784&quot;&gt;
              &lt;img alt&#x3D;&quot;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&quot; src&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;tweets_media&#x2F;1322221511776886784-Ell5c5LXYAIR0ZK.jpg&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1322221511776886784&quot;&gt;&lt;b&gt;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              COVID NUMBERS • JS 
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1324016434574491650&quot;&gt;
              &lt;img alt&#x3D;&quot;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&quot; src&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;tweets_media&#x2F;1324016434574491650-El_Z7Q-WMAcvop7.jpg&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1324016434574491650&quot;&gt;&lt;b&gt;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              Merging threads https:&#x2F;&#x2F;twitter.com&#x2F;zemlanin&#x2F;status&#x2F;1298526710556762119 
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;  &lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1330140655281172492&quot;&gt;
              &lt;img alt&#x3D;&quot;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&quot; src&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;tweets_media&#x2F;1330140655281172492-EnWb3pgXUAA4JYH.jpg&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;&#x2F;zemlanin&#x2F;status&#x2F;1330140655281172492&quot;&gt;&lt;b&gt;Anton 🌻 @zemlanin@devua.club on Twitter (archived)&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i class&#x3D;&quot;truncated&quot;&gt;
              CHARTD FTW 
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ul&gt;
&lt;p&gt;Самый простой из виджетов выше — чуточку более красивая реализация &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;wakemeupwhenmarchends.com&quot;&gt;wakemeupwhenmarchends.com&lt;&#x2F;a&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-12-4nwS3hsiRt:og-src&quot; id&#x3D;&quot;rfn:post-2020-12-4nwS3hsiRt:og-src&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; — работает только с датами и (примитивнейшим) UI, так что он идеально подойдёт как «мой первый виджет»&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-12-4nwS3hsiRt:tbc&quot; id&#x3D;&quot;rfn:post-2020-12-4nwS3hsiRt:tbc&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;основы-интерфейса&quot;&gt;Основы интерфейса&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;VqMlNZZV5xns8cGQYdOprfwptj.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Редактор скриптов в Scriptable довольно базовый, но самодостаточный — есть подсветка и автодополнение, есть консоль, есть документация, есть запуск скрипта прямо в редакторе (тапом по ▶️ или нажатием Cmd-R, если подключена внешняя клавиатура)&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;мой-первый-виджет&quot;&gt;Мой первый виджет&lt;&#x2F;h2&gt;
&lt;p&gt;Для того, чтобы вывести самый базовый виджет, достаточно трёх строк кода (даже двух, если забить на дебаг):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widget &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ListWidget&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;presentSmall&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Script&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setWidget&lt;&#x2F;span&gt;(widget);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const widget &#x3D; new ListWidget();&lt;&#x2F;code&gt; создаёт объект виджета, который мы будем изменять для отрисовки календаря&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;widget.presentSmall();&lt;&#x2F;code&gt; показывает окно предпросмотра с маленьким форматом виджета&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Script.setWidget(widget);&lt;&#x2F;code&gt; обновляет виджет на домашнем экране&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Вбиваем три эти строки в редактор, нажимаем ▶️, и видим наш виджет — белый (или чёрный, если включен Dark Mode) квадрат:&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;eDwe93C4r4MGa2fb3MYe5D7WlZ.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;ommx3E73nLkowAcvWWfLEyFjJn.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;this-house-is-not-a-dom&quot;&gt;This House is not a DOM&lt;&#x2F;h2&gt;
&lt;p&gt;API для работы с виджетами в Scriptable &lt;em&gt;похож&lt;&#x2F;em&gt; на браузерный DOM, но не DOM. Например:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;все элементы виджета создаются как дочерние элементы самого виджета или его элементов. Другими словами, вместо браузерного «создать элемент и присоединить его к родителю» (&lt;code&gt;const child &#x3D; document.createElement(&quot;span&quot;); parent.appendChildren(child)&lt;&#x2F;code&gt;), в Scriptable используется «создать дочерний элемент у родителя» (&lt;code&gt;const child &#x3D; parent.addText()&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;стили элементов (цвета, шрифты, отступы) задаются отдельными методами, а не свойством &lt;code&gt;style&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;для определения порядка элементов используются &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;widgetstack&#x2F;&quot;&gt;&lt;code&gt;WidgetStack&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; с заданным горизонтальным или вертикальным layout’ом&lt;&#x2F;li&gt;
&lt;li&gt;отступы &lt;em&gt;между&lt;&#x2F;em&gt; элементами задаются с помощью &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;widgetspacer&#x2F;&quot;&gt;&lt;code&gt;WidgetSpacer&lt;&#x2F;code&gt;’ов&lt;&#x2F;a&gt;, а не &lt;code&gt;margin&lt;&#x2F;code&gt;’ов (которых нет ни в Scriptable, ни в SwiftUI, который используется для рендера виджетов)&lt;&#x2F;li&gt;
&lt;li&gt;отступы &lt;em&gt;внутри&lt;&#x2F;em&gt; &lt;code&gt;WidgetStack&lt;&#x2F;code&gt; задаются методом &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;widgetstack&#x2F;#-setpadding&quot;&gt;&lt;code&gt;setPadding(top, leading, bottom, trailing)&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-12-4nwS3hsiRt:ltr&quot; id&#x3D;&quot;rfn:post-2020-12-4nwS3hsiRt:ltr&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;li&gt;
&lt;li&gt;вместо строчных шрифтов, цветов и размеров, Scriptable использует классы &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;font&#x2F;&quot;&gt;&lt;code&gt;Font&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;color&#x2F;&quot;&gt;&lt;code&gt;Color&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;size&#x2F;&quot;&gt;&lt;code&gt;Size&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. У первых двух классов есть множество статических методов для стандартных значений (вроде &lt;code&gt;Font.title()&lt;&#x2F;code&gt; и &lt;code&gt;Color.red()&lt;&#x2F;code&gt;), которые выглядят консистентно с остальными виджетами и приложениями&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Учитывая всё это, можно приступить к UI виджета. Наш виджет будет выглядеть как три строки отцентрованного текста на белом фоне:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;белый текст «Март» на красном фоне&lt;&#x2F;li&gt;
&lt;li&gt;БОЛЬШОЙ чёрный текст с «сегодняшним числом»&lt;&#x2F;li&gt;
&lt;li&gt;чёрный текст с сегодняшним днём недели&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Чтобы не повторяться, определим функцию &lt;code&gt;addTextRow&lt;&#x2F;code&gt;, которая добавит в виджет &lt;code&gt;WidgetStack&lt;&#x2F;code&gt; с заданными текстом и фоном и вернёт &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;widgettext&#x2F;&quot;&gt;&lt;code&gt;WidgetText&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; чтобы можно было добавить тексту дополнительных стилей:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widget &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ListWidget&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;text, backgroundColor &#x3D; Color.white()&lt;&#x2F;span&gt;) {  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; textStack &#x3D; widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
  textStack.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; backgroundColor;
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutHorizontally&lt;&#x2F;span&gt;();

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widgetText &#x3D; textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addText&lt;&#x2F;span&gt;(text);
  widgetText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;black&lt;&#x2F;span&gt;();
  
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; widgetText;
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; titleText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Март&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;red&lt;&#x2F;span&gt;());

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;999+&#x60;&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeekText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;воскресенье&quot;&lt;&#x2F;span&gt;);

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;presentSmall&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Script&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setWidget&lt;&#x2F;span&gt;(widget);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Нажимаем ▶️…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;0lFw5jL0GGwpIGpNdR8BHfbSY5.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ай, забыл про стандартные стили виджета… Чтобы красная полоса марта была на всю ширину виджета, надо сбросить стандартный padding. Добавим &lt;code&gt;widget.setPadding(0, 0, 0, 0);&lt;&#x2F;code&gt; сразу после первой строки скрипта:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;sWE4VEBdpIXHAHDCnD9nicEdLQ.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Добавим &lt;code&gt;spacer&lt;&#x2F;code&gt;’ов и стилей тексту…&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widget &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ListWidget&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);
widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;text, backgroundColor &#x3D; Color.white()&lt;&#x2F;span&gt;) {  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; textStack &#x3D; widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
  textStack.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; backgroundColor;
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutHorizontally&lt;&#x2F;span&gt;();

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widgetText &#x3D; textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addText&lt;&#x2F;span&gt;(text);
  widgetText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;black&lt;&#x2F;span&gt;();
  
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; widgetText;
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; titleText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Март&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;red&lt;&#x2F;span&gt;());
titleText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();
titleText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;font&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Font&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;title2&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;999+&#x60;&lt;&#x2F;span&gt;);
dayText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;font&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Font&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;systemFont&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt;);
dayText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;minimumScaleFactor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0.5&lt;&#x2F;span&gt;;

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeekText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;воскресенье&quot;&lt;&#x2F;span&gt;);

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;presentSmall&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Script&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setWidget&lt;&#x2F;span&gt;(widget);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;И видим красивый, хоть и с неправильными данными, виджет:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;dYsxKIWfh8W4Ppc8rahzenxxKr.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;time-is-haaaaaard&quot;&gt;Time is haaaaaard&lt;&#x2F;h2&gt;
&lt;p&gt;Пора разбираться с данными в виджете… Месяц в нём никогда не меняется, так что оставляем его как константу&lt;&#x2F;p&gt;
&lt;p&gt;День месяца немного сложнее. Нам нужно посчитать количество суток с полуночи первого марта 2020 года и округлить его в большую сторону (т.е., в шесть утра второго марта прошло &lt;code&gt;1.25&lt;&#x2F;code&gt; суток, округляем в большую сторону до &lt;code&gt;2&lt;&#x2F;code&gt; и получаем правильный день месяца)&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;2020-03-01T00:00:00&quot;&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; должно вывести полночь первого марта в текущем часовом поясе&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;HOUR&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1000&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt;;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; day &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;ceil&lt;&#x2F;span&gt;((&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;() - &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt;) &#x2F; (&lt;span class&#x3D;&quot;hljs-number&quot;&gt;24&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;HOUR&lt;&#x2F;span&gt;));

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(day);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;С днём недели можно либо забить и захардкодить список дней недели прямо в скрипт (как это было сделано для сайта), либо немного заморочиться и использовать &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;docs.scriptable.app&#x2F;dateformatter&#x2F;&quot;&gt;&lt;code&gt;DateFormatter&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. Второй способ &lt;del&gt;требует меньше кода&lt;&#x2F;del&gt; &lt;em&gt;проще скейлить&lt;&#x2F;em&gt;, так что я выберу его…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;DateFormatter&lt;&#x2F;code&gt; конвертирует дату в строчку и обратно, причём строчка может быть на любом языке и&#x2F;или в произвольном формате. Открываем &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;nsdateformatter.com&quot;&gt;NSDateFormatter.com&lt;&#x2F;a&gt;, находим подходящий нам “&lt;code&gt;EEEE&lt;&#x2F;code&gt; — Tuesday — The wide name of the day of the week” и форматируем сегодняшнюю дату в день недели:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeekFormatter &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;DateFormatter&lt;&#x2F;span&gt;();
dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;locale&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ru&quot;&lt;&#x2F;span&gt;
dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;dateFormat&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;EEEE&quot;&lt;&#x2F;span&gt;;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeek &#x3D; dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;string&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;());
&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(dayOfWeek);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Используем &lt;code&gt;day&lt;&#x2F;code&gt; и &lt;code&gt;dayOfWeek&lt;&#x2F;code&gt; в вызовах &lt;code&gt;addTextRow&lt;&#x2F;code&gt; и проверяем результат:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;2020-03-01T00:00:00&quot;&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; должно вывести полночь первого марта в текущем часовом поясе&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;HOUR&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1000&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt;;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; day &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Math&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;ceil&lt;&#x2F;span&gt;((&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;() - &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;MARCH_FIRST&lt;&#x2F;span&gt;) &#x2F; (&lt;span class&#x3D;&quot;hljs-number&quot;&gt;24&lt;&#x2F;span&gt; * &lt;span class&#x3D;&quot;hljs-variable constant_&quot;&gt;HOUR&lt;&#x2F;span&gt;));

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(day);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeekFormatter &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;DateFormatter&lt;&#x2F;span&gt;();
dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;locale&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ru&quot;&lt;&#x2F;span&gt;
dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;dateFormat&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;EEEE&quot;&lt;&#x2F;span&gt;;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeek &#x3D; dayOfWeekFormatter.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;string&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Date&lt;&#x2F;span&gt;());
&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;console&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;log&lt;&#x2F;span&gt;(dayOfWeek);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widget &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ListWidget&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);
widget.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;text, backgroundColor &#x3D; Color.white()&lt;&#x2F;span&gt;) {  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; textStack &#x3D; widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addStack&lt;&#x2F;span&gt;();
  textStack.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;backgroundColor&lt;&#x2F;span&gt; &#x3D; backgroundColor;
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setPadding&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;4&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;);

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;layoutHorizontally&lt;&#x2F;span&gt;();

  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; widgetText &#x3D; textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addText&lt;&#x2F;span&gt;(text);
  widgetText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;black&lt;&#x2F;span&gt;();
  
  textStack.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);
  
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; widgetText;
}

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; titleText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Март&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;red&lt;&#x2F;span&gt;());
titleText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;textColor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Color&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;white&lt;&#x2F;span&gt;();
titleText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;font&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Font&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;title2&lt;&#x2F;span&gt;();

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${day}&lt;&#x2F;span&gt;&#x60;&lt;&#x2F;span&gt;);
dayText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;font&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Font&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;systemFont&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;60&lt;&#x2F;span&gt;);
dayText.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;minimumScaleFactor&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;0.5&lt;&#x2F;span&gt;;

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addSpacer&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;);

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; dayOfWeekText &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;addTextRow&lt;&#x2F;span&gt;(dayOfWeek);

widget.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;presentSmall&lt;&#x2F;span&gt;();

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Script&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;setWidget&lt;&#x2F;span&gt;(widget);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;CWVn7cPJcWxJHsZjC5joc6yTzc.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;🎉&lt;&#x2F;p&gt;
&lt;p&gt;Осталось только добавить наш виджет на домашний экран&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-12-4nwS3hsiRt:4&quot; id&#x3D;&quot;rfn:post-2020-12-4nwS3hsiRt:4&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; и готово&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;GoHsE643GxoniurzxmWwW4f6Y8.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;WHkXtqrhu1x9Tdj6fIMYKsY2fW.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;iAhYjIuJYS8gtAqDS8VO0wEuDT.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;IA955peYzSTO6ibzgBImKdcTO4.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;epCrLgGDzS9KfCDTxctmbuNAXl.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;Ku91gYybTDTEzlIjGzCBTAQb5D.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;&lt;&#x2F;ul&gt;
&lt;h2 id&#x3D;&quot;and-the-clever-depart&quot;&gt;…and the &lt;del&gt;Clever&lt;&#x2F;del&gt; Depart&lt;&#x2F;h2&gt;
&lt;p&gt;Scriptable умеет общаться с файлами и HTTP-эндпоинтами, так что эта запись (и твиттер-тред в начале) лишь поверхностно описывает возможности виджетов. Да и писать свои виджеты совсем необязательно — в Scriptable недавно появилась какая-никакая галерея виджетов&lt;&#x2F;p&gt;
&lt;p&gt;Спасибо за внимание&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2020-12-4nwS3hsiRt:og-src&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;wmuwme&quot;&gt;Исходники&lt;&#x2F;a&gt; сайта&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-12-4nwS3hsiRt:og-src&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-12-4nwS3hsiRt:tbc&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Вторым виджетом стал бы виджет со статистикой COVID, с его HTTP-запросами, графиком и поддержкой Dark Mode… &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;devua.club&#x2F;@zemlanin&quot;&gt;Пишите&lt;&#x2F;a&gt;, если интересно&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-12-4nwS3hsiRt:tbc&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-12-4nwS3hsiRt:ltr&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Для письменностей слева-на-право, вроде кириллицы и латиницы, &lt;code&gt;leading&lt;&#x2F;code&gt; &#x3D; &lt;code&gt;left&lt;&#x2F;code&gt;, &lt;code&gt;trailing&lt;&#x2F;code&gt; &#x3D; &lt;code&gt;right&lt;&#x2F;code&gt;, так что порядок аргументов «против часовой» в отличии от «по часовой» в CSS&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-12-4nwS3hsiRt:ltr&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-12-4nwS3hsiRt:4&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;После добавления виджета Scriptable, его нужно ещё раз нажать, чтобы выбрать скрипт, который будет рендерить его содержание&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-12-4nwS3hsiRt:4&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/widgetarian-wmuwme.html</link>
      <guid isPermaLink="false">post-2020-12-4nwS3hsiRt</guid>
      <pubDate>Sun, 13 Dec 2020 14:11:00 GMT</pubDate>
    </item>
    <item>
      
      <description>
        &lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;rachelbythebay.com&#x2F;w&#x2F;2020&#x2F;08&#x2F;14&#x2F;jobs&#x2F;&quot;&gt;We are a spectrum of jobs, not just one&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The problem, then, is when you hire one for the other job. In one case, you might get someone who&amp;#39;s in completely over their head when they can&amp;#39;t find an existing module or snippet somewhere. In the other case, you might end up with someone who thinks they&amp;#39;re surrounded by a bunch of people who are speaking a different language, and feels very much alone.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;

        &lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;post-2020-08-bqeCNPyvsd.html&quot;&gt;&amp;infin;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
      </description>
      <link>https://zemlan.in/post-2020-08-bqeCNPyvsd.html</link>
      <guid isPermaLink="false">post-2020-08-bqeCNPyvsd</guid>
      <pubDate>Sat, 15 Aug 2020 14:40:00 GMT</pubDate>
    </item>
    <item>
      <title>Заброшенное в карантин: Pepperoni</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;NRegUL8rrpg4f1NhFoKxhnAc5k&#x2F;fit1000.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Слишком много времени, слишком мало терпения&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Если убрать из жизни дорогу на работу&#x2F;с работы, «пойти в кафе», и «поехать в центр, чтобы повидаться с друзьями», то остаётся &lt;em&gt;много&lt;&#x2F;em&gt; свободного времени. Полностью занимать его «поглощением контента» (будь то кино&#x2F;сериалы, книги, подкасты, музыка…) кажется нездоровым, игр надолго не хватает, звонки «как жизнь? — по большому счёту, так же, как и последние пять месяцев&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-07-QwZaiC1Rwd:1&quot; id&#x3D;&quot;rfn:post-2020-07-QwZaiC1Rwd:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;» быстро надоедают…&lt;&#x2F;p&gt;
&lt;p&gt;Так что мой выбор &lt;del&gt;яда&lt;&#x2F;del&gt; хобби — это делать то же самое, что на работе, но для себя. А так как для себя, то больше никому не надо чтобы я довёл дело до конца&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-07-QwZaiC1Rwd:precooked&quot; id&#x3D;&quot;rfn:post-2020-07-QwZaiC1Rwd:precooked&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Например, никто не будет подталкивать добить до конца выступление для митапа, который должен был пройти в марте&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;proxet_com&#x2F;status&#x2F;1234823528622501888&quot;&gt;
              &lt;img alt&#x3D;&quot;Proxet on Twitter&quot; src&#x3D;&quot;https:&#x2F;&#x2F;pbs.twimg.com&#x2F;media&#x2F;ESL5dIOWkAEm51x.jpg:large&quot;&gt;
          &lt;&#x2F;a&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.com&#x2F;proxet_com&#x2F;status&#x2F;1234823528622501888&quot;&gt;&lt;b&gt;Proxet on Twitter&lt;&#x2F;b&gt;&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i&gt;
              Rails Reactor is happy to announce the upcoming Python Tonight.&lt;br&gt;SPEAKERS:&lt;br&gt;👨‍💻Vitaliy Radchenko - Intro to Apache Airflow&lt;br&gt;👨‍💻Anton Verinov - Cook with What You Have&lt;br&gt;👨‍💻Denys Serhiienko - Optimization. A little adventure.&lt;br&gt; Need to register  &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;t.co&#x2F;NAEEzkW6wN&quot;&gt;https:&#x2F;&#x2F;t.co&#x2F;NAEEzkW6wN&lt;&#x2F;a&gt;
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;
&lt;p&gt;или дописать-таки кучу слов про code review&lt;&#x2F;p&gt;
&lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;RuUKHVSC8r9iZg0MiUzTnFZVW0.png&quot;&gt;
              &lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;RuUKHVSC8r9iZg0MiUzTnFZVW0&#x2F;fit1000.png&quot; width&#x3D;&quot;703&quot; height&#x3D;&quot;475&quot;&gt;
          &lt;&#x2F;a&gt;


        &lt;figcaption&gt;
            &lt;i&gt;
              Half outlined and half ready-ish. This is gonna loooong…
            &lt;&#x2F;i&gt;
        &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;или причесать вроде юзабельный (хоть и довольно бесполезный) side project для публики&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;DYwbyc1k99pQksBxjWkCMY0M8c&#x2F;fit1000.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Но всё это можно &lt;del&gt;похоронить&lt;&#x2F;del&gt; заморозить в бложек. Авось даже в сыром виде оно кому-то пригодится&lt;&#x2F;p&gt;
&lt;p&gt;Начну с выступления для Python митапа…&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;pitch&quot;&gt;Pitch&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Суть:&lt;&#x2F;strong&gt; сделать что-то полезное исключительно средствами стандартной библиотеки Python&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Почему:&lt;&#x2F;strong&gt; легче разворачивать, распространять, меньше поддерживать (потому что меньше обновляемых частей)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Что делаем:&lt;&#x2F;strong&gt; мониторинг веб-страниц на обновление. Юзер вбивает ссылку и опциональный фильтр для данных, а скрипт периодически пингует страницу и уведомляет об изменениях&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Структура выступления:&lt;&#x2F;strong&gt; преподнести процесс написания скрипта как процесс готовки еды, с модулями как ингредиентами. Наиболее подходящим “блюдом” показалась пицца&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;на-что-хватило&quot;&gt;На что хватило?&lt;&#x2F;h2&gt;
&lt;p&gt;Сил и времени перед отменой митапа хватило на написание outline’а и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;pepperoni&quot;&gt;самого скрипта&lt;&#x2F;a&gt;, который даже пригодился когда ждал замены батарейки в телефоне:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;9G4uKBEJH5Ks9Fr7TSWXvuwp27&#x2F;fit1000.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;По outline’у, все импорты разделил на четыре группы:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-python&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;# spices&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; os
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; time
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; platform

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;# sause&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; re
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; difflib
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; textwrap
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; html.parser

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;# meat&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; urllib.request

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;# dough&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; logging
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; argparse
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;мясо&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-07-QwZaiC1Rwd:cheese&quot; id&#x3D;&quot;rfn:post-2020-07-QwZaiC1Rwd:cheese&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; — основная часть скрипта, которую выполняет &lt;code&gt;urllib.request&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;тесто — интерфейс, за который юзер будет «держать» скрипт&lt;ul&gt;
&lt;li&gt;&lt;code&gt;logging&lt;&#x2F;code&gt; для красивого вывода с таймстампами&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;argparse&lt;&#x2F;code&gt; для ввода опций для «соуса»&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;соус — всякая «вкусовщина», без которых «мясо» будет съедобно, но не то. В основном, это удобства для «что отслеживать на странице?»&lt;&#x2F;li&gt;
&lt;li&gt;специи — модули, которые пойдут в «любое блюдо». А правильно применённые специи могут быть небольшим «секретным ингредиентом», вроде звукового уведомления:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-python&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; platform.system() &#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Darwin&quot;&lt;&#x2F;span&gt;:
    parser.add_argument(
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;--sound&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;-s&quot;&lt;&#x2F;span&gt;,
        dest&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;mac_sound&quot;&lt;&#x2F;span&gt;,
        default&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;pop&quot;&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;# &#x60;ls &#x2F;System&#x2F;Library&#x2F;Sounds&#x2F;&#x60;&lt;&#x2F;span&gt;
        choices&#x3D;[
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;basso&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;blow&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;bottle&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;frog&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;funk&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;glass&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;hero&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;morse&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;ping&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;pop&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;purr&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;sosumi&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;submarine&quot;&lt;&#x2F;span&gt;,
            &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;tink&quot;&lt;&#x2F;span&gt;,
        ],
        &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;type&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;str&lt;&#x2F;span&gt;,
        &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;help&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;sound to play (macOS only)&quot;&lt;&#x2F;span&gt;,
    )
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-python&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; mac_sound:
    os.system(
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;f&quot;afplay &#x2F;System&#x2F;Library&#x2F;Sounds&#x2F;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;{mac_sound.title()}&lt;&#x2F;span&gt;.aiff 2&amp;gt; &#x2F;dev&#x2F;null&quot;&lt;&#x2F;span&gt;
    )
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;else&lt;&#x2F;span&gt;:
    &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;print&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;\a&quot;&lt;&#x2F;span&gt;, end&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&lt;&#x2F;span&gt;, flush&#x3D;&lt;span class&#x3D;&quot;hljs-literal&quot;&gt;True&lt;&#x2F;span&gt;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Кроме этих четырёх ингредиентов, хотел упомянуть что в стандартной библиотеке есть «кухонные приборы» — модули вроде &lt;code&gt;doctest&lt;&#x2F;code&gt;, которые упрощают разработку:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-python&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;def&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;query_html&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;html, query, regex&lt;&#x2F;span&gt;):
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&quot;&quot;
    &amp;gt;&amp;gt;&amp;gt; html &#x3D; &quot;&amp;lt;ul&amp;gt;&amp;lt;li&amp;gt;A&amp;lt;&#x2F;li&amp;gt;&amp;lt;li id&#x3D;&#39;b&#39;&amp;gt;B&amp;lt;&#x2F;li&amp;gt;&amp;lt;&#x2F;ul&amp;gt;&amp;lt;ol&amp;gt;&amp;lt;li&amp;gt;C&amp;lt;&#x2F;li&amp;gt;&amp;lt;li class&#x3D;&#39;d&#39;&amp;gt;D&amp;lt;&#x2F;li&amp;gt;&amp;lt;&#x2F;ol&amp;gt;&quot;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;li&quot;, None)
    &#39;A&#39;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;#b&quot;, None)
    &#39;B&#39;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;ol li&quot;, None)
    &#39;C&#39;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;.d&quot;, None)
    &#39;D&#39;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;ul li&quot;, &quot;B|X&quot;)
    &#39;B&#39;
    &amp;gt;&amp;gt;&amp;gt; query_html(html, &quot;ol li&quot;, &quot;B|X&quot;)
    &quot;&quot;&quot;&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; query:
        html_parser &#x3D; HTMLParser(query, regex)
        html_parser.feed(html)
        &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; html_parser.match
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;elif&lt;&#x2F;span&gt; regex:
        match &#x3D; re.search(regex, html)
        &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; match:
            &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; match.group(&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;)

    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;None&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;После всего этого кода, в качестве outro, сказал бы о штуках, которых не хватает (вроде форматтера и хоть какой-то работы с изображениями), плюс отметил бы что&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Конечно, в PyPI можно добыть ингредиенты покачественнее:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;re&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;regex&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;argparse&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;click&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;urllib.request&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;request&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;html.parser&lt;&#x2F;code&gt; -&amp;gt; &lt;code&gt;BeautifulSoup&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Но и стандартного набора более чем достаточно для вкусного блюда&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Исходники всего скрипта доступны на &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;pepperoni&quot;&gt;гитхабе&lt;&#x2F;a&gt; и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;hiYrRP1BPOmTIMapmYPJ0Ai6v6.py&quot;&gt;здесь&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Спасибо, что дочитали до конца&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2020-07-QwZaiC1Rwd:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Уже пять месяцев?.. Чёёёёёрт…&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-07-QwZaiC1Rwd:1&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-07-QwZaiC1Rwd:precooked&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Или не забросил начинания как, честно говоря, забросил &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;precooked.html&quot;&gt;готовку дома&lt;&#x2F;a&gt;. Ну, то есть, готовлю чаще, чем в &quot;Before Times&quot;, но &lt;em&gt;намного&lt;&#x2F;em&gt; меньше, чем в момент написания той заметки&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-07-QwZaiC1Rwd:precooked&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-07-QwZaiC1Rwd:cheese&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;или «мясо + сыр», или «протеин»…&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-07-QwZaiC1Rwd:cheese&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/quarantined-pepperoni.html</link>
      <guid isPermaLink="false">post-2020-07-QwZaiC1Rwd</guid>
      <pubDate>Sat, 11 Jul 2020 10:00:00 GMT</pubDate>
    </item>
    <item>
      <title>Reuse500</title>
      <description>
        &lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;wmd7wLB04Ce5QqiF611dl3fVVO&#x2F;fit1600.jpeg&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Год назад я купил простенький кликер, Logitech R500. Работает он как двухкнопочная клавиатура с лазерной указкой. Сейчас, когда возможностей для публичных выступлений &lt;em&gt;поменьше&lt;&#x2F;em&gt;, он стал довольно бесполезным&lt;&#x2F;p&gt;
&lt;p&gt;Вне презентаций мало ситуаций, в которых можно обойтись только кнопками &quot;влево&quot; и &quot;вправо&quot;. Но, во время видеозвонков, когда можно обойтись &lt;em&gt;другими&lt;&#x2F;em&gt; двумя кнопками — &quot;Вкл&#x2F;выкл камеру&quot; и &quot;Вкл&#x2F;выкл микрофон&quot;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-05-FeByPfyf0t:1&quot; id&#x3D;&quot;rfn:post-2020-05-FeByPfyf0t:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. Нужно только сопоставить одни с другими&lt;&#x2F;p&gt;
&lt;p&gt;И может твой рабочий видеочат может в настройки сочетаний клавиш, но не BlueJeans. BlueJeans может только &lt;kbd&gt;M&lt;&#x2F;kbd&gt; для переключения микрофона и &lt;kbd&gt;V&lt;&#x2F;kbd&gt; для переключения камеры&lt;&#x2F;p&gt;
&lt;p&gt;macOS умеет, на уровне системы, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;ru-ru&#x2F;guide&#x2F;mac-help&#x2F;mchlp2271&#x2F;mac&quot;&gt;вешать и переопределять сочетания клавиш&lt;&#x2F;a&gt; для элементов меню в любом приложении. Например, для выхода из Safari можно повесить &lt;kbd&gt;⌥&lt;&#x2F;kbd&gt;&lt;kbd&gt;⌘&lt;&#x2F;kbd&gt;&lt;kbd&gt;Q&lt;&#x2F;kbd&gt; вместо стандартной &lt;kbd&gt;⌘&lt;&#x2F;kbd&gt;&lt;kbd&gt;Q&lt;&#x2F;kbd&gt; (которое слишком близко к закрывающему вкладки &lt;kbd&gt;⌘&lt;&#x2F;kbd&gt;&lt;kbd&gt;W&lt;&#x2F;kbd&gt;)&lt;&#x2F;p&gt;
&lt;ul data-gallery&#x3D;&quot;&quot; style&#x3D;&quot;list-style:none;padding:0&quot;&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;iUqEq2iQT5Mf3k2oS4I00n7xUO.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;DT3l889KnKbWHAsF809laROgVa.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Но и тут BlueJeans отличился тем, что в его меню нет пунктов про камеру и&#x2F;или микрофон… Поэтому придётся пользоваться сторонним софтом. Так как у меня уже был установлен Karabiner&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2020-05-FeByPfyf0t:2&quot; id&#x3D;&quot;rfn:post-2020-05-FeByPfyf0t:2&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, поковырялся &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;karabiner-elements.pqrs.org&#x2F;docs&#x2F;json&#x2F;complex-modifications-manipulator-definition&#x2F;&quot;&gt;его документацию&lt;&#x2F;a&gt; и &lt;a href&#x3D;&quot;karabiner:&#x2F;&#x2F;karabiner&#x2F;assets&#x2F;complex_modifications&#x2F;import?url&#x3D;data:application&#x2F;json;base64,eyJ0aXRsZSI6Ik1pYyBhbmQgdmlkZW8gdG9nZ2xlIG9uIGFycm93IGtleXMgaW4gQmx1ZUplYW5zIiwicnVsZXMiOlt7Im1hbmlwdWxhdG9ycyI6W3siZnJvbSI6eyJrZXlfY29kZSI6InJpZ2h0X2Fycm93In0sInRvIjpbeyJrZXlfY29kZSI6Im0iLCJtb2RpZmllcnMiOltdfV0sInR5cGUiOiJiYXNpYyIsImNvbmRpdGlvbnMiOlt7InR5cGUiOiJmcm9udG1vc3RfYXBwbGljYXRpb25faWYiLCJidW5kbGVfaWRlbnRpZmllcnMiOlsiXmNvbVxcLmJsdWVqZWFuc25ldFxcLkJsdWUkIl19XX1dLCJkZXNjcmlwdGlvbiI6IkNoYW5nZSBSaWdodCBhcnJvdyB0byBNIGluIEJsdWVKZWFucyJ9LHsibWFuaXB1bGF0b3JzIjpbeyJ0eXBlIjoiYmFzaWMiLCJmcm9tIjp7ImtleV9jb2RlIjoibGVmdF9hcnJvdyJ9LCJjb25kaXRpb25zIjpbeyJ0eXBlIjoiZnJvbnRtb3N0X2FwcGxpY2F0aW9uX2lmIiwiYnVuZGxlX2lkZW50aWZpZXJzIjpbIl5jb21cXC5ibHVlamVhbnNuZXRcXC5CbHVlJCJdfV0sInRvIjpbeyJrZXlfY29kZSI6InYiLCJyZXBlYXQiOmZhbHNlfV19XSwiZGVzY3JpcHRpb24iOiJDaGFuZ2UgTGVmdCBhcnJvdyB0byBWIGluIEJsdWVKZWFucyJ9XX0&#x3D;&quot;&gt;накликал правила&lt;&#x2F;a&gt; &quot;когда фокус на BlueJeans, воспринимай &lt;kbd&gt;→&lt;&#x2F;kbd&gt; как &lt;kbd&gt;M&lt;&#x2F;kbd&gt; и &lt;kbd&gt;←&lt;&#x2F;kbd&gt; как &lt;kbd&gt;V&lt;&#x2F;kbd&gt;&quot;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-json&quot;&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;title&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Mic and video toggle on arrow keys in BlueJeans&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;rules&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;description&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Change Right arrow to M in BlueJeans&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;manipulators&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt;
        &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;from&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;key_code&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;right_arrow&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;to&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;key_code&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;m&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;type&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;basic&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;conditions&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt;
            &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
              &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;type&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;frontmost_application_if&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
              &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;bundle_identifiers&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;^com\\.bluejeansnet\\.Blue$&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
            &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
        &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;description&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Change Left arrow to V in BlueJeans&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;manipulators&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt;
        &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;type&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;basic&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;from&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;key_code&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;left_arrow&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;to&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;key_code&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;v&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;conditions&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt;
            &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
              &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;type&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;frontmost_application_if&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;,&lt;&#x2F;span&gt;
              &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;bundle_identifiers&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;[&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;^com\\.bluejeansnet\\.Blue$&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
            &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
          &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
        &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
      &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
    &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;]&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;hr&gt;
&lt;p&gt;Не знаю, как красиво закончить… Была валяющаяся без дела железка, была лениво написанная софтина. Щепотка JSON&#39;а в правильном месте — и железка полезнее, и софтина удобнее ✨🧙‍♂️&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2020-05-FeByPfyf0t:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;К сожалению, не все догадываются о том, что микрофон на ноутбуке можно&#x2F;нужно выключать, когда набираешь текст во время звонка&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-05-FeByPfyf0t:1&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2020-05-FeByPfyf0t:2&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Чтобы перенастроить Ctrl-Win-Alt (≈ Control-Command-Option) на майкрософтовской клавиатуре в более маковские Control-Option-Command&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2020-05-FeByPfyf0t:2&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/reuse500.html</link>
      <guid isPermaLink="false">post-2020-05-FeByPfyf0t</guid>
      <pubDate>Wed, 13 May 2020 09:34:00 GMT</pubDate>
    </item>
    <item>
      <title>Готовить&#x2F;ся</title>
      <description>
        &lt;p&gt;Я редко готовлю (если не считать утреннего кофе). В основном, еда — это либо офисные обеды, либо кафе, либо доставка&lt;&#x2F;p&gt;
&lt;p&gt;В ноябре-декабре прошлого года, YouTube начал активно рекомендовать кулинарные видео, в частности, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;basicswithbabish.co&#x2F;&quot;&gt;Basics with Babish&lt;&#x2F;a&gt;. Поддавшись алгоритму, я пересмотрел несколько десятков таких видео&lt;&#x2F;p&gt;
&lt;p&gt;Так что когда приближающейся новогодние праздники начали угрожать привычному пропитанию (потому что выходные, праздничный режим работы кафе, нагруженная доставка), решил первую неделю года готовить дома&lt;&#x2F;p&gt;
&lt;p&gt;Получилось более-менее сносно — &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;74tZ-yOOPy0&quot;&gt;куриный суп&lt;&#x2F;a&gt;, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;qoHnwOHLiMk&quot;&gt;карбонара&lt;&#x2F;a&gt;, крылышки, что-то ещё, о чём уже забыл… Хотя опасения не оправдались — поесть в кафе и заказать доставку можно было довольно легко… &lt;&#x2F;p&gt;
&lt;p&gt;После этого, снова вернулся к привычному офисные-обеды-кафе-доставка. Ненадолго, на пару месяцев…&lt;&#x2F;p&gt;
&lt;hr&gt;
&lt;p&gt;Не мог подумать, что первая неделя года будет тренировкой&lt;&#x2F;p&gt;

        
      </description>
      <link>https://zemlan.in/precooked.html</link>
      <guid isPermaLink="false">post-2020-03-ontN1uSG6j</guid>
      <pubDate>Sun, 22 Mar 2020 12:45:00 GMT</pubDate>
    </item>
    <item>
      <title>Усатый ядерщик</title>
      <description>
        &lt;p&gt;  &lt;&#x2F;p&gt;&lt;figure class&#x3D;&quot;card&quot;&gt;
        &lt;iframe src&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;vEUylyDMFIA&quot; frameborder&#x3D;&quot;0&quot; width&#x3D;&quot;480&quot; height&#x3D;&quot;360&quot; allow&#x3D;&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; allowfullscreen&#x3D;&quot;1&quot;&gt;&lt;&#x2F;iframe&gt;


      &lt;figcaption&gt;
          &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;vEUylyDMFIA&quot;&gt;&lt;b&gt;React with mustaches&lt;&#x2F;b&gt; • React Kyiv&lt;br&gt;&lt;&#x2F;a&gt;
            &lt;i&gt;
              Are there good parts inside an abandoned project? What could we learn from it? Could we apply some patterns in our current projects?
            &lt;&#x2F;i&gt;
      &lt;&#x2F;figcaption&gt;
  &lt;&#x2F;figure&gt;
&lt;p&gt;&lt;&#x2F;p&gt;&lt;p&gt;&lt;em&gt;Выковыривая полезные идеи из заброшенного модуля&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Адаптировано из выступления “React with mustaches” на React Kyiv (&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;youtu.be&#x2F;vEUylyDMFIA&quot;&gt;видео&lt;&#x2F;a&gt; и &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;7TZEn3OXO0kfBsTz213YNxhM4W.pdf&quot;&gt;слайды&lt;&#x2F;a&gt;)&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Бывает так, что гоняешься не за тем, за чем стоило бы. В разработке, это зачастую выражается в странных решениях временных или надуманных проблем. Для таких решений пишется код, который либо никогда не увидит продакшена, либо довольно быстро будет удалён за ненадобностью&lt;&#x2F;p&gt;
&lt;p&gt;Повезёт, если такой код поможет не только тебе&lt;&#x2F;p&gt;
&lt;p&gt;Но повезёт &lt;em&gt;ещё больше&lt;&#x2F;em&gt;, если кроме одноразового кода останется опыт, который поможет решить более стоящие проблемы&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;проблема&quot;&gt;Проблема&lt;&#x2F;h2&gt;
&lt;p&gt;Проект, над которым я тогда работал, использовал Python на бекенде и React на фронтенде. Из-за SEO и &lt;em&gt;почтительного&lt;&#x2F;em&gt; возраста кодовой базы, React использовался не для SPA, а для progressive enhancement — бекенд, на основе Mako-шаблонов, отдавал HTML со полным контентом страницы&lt;&#x2F;p&gt;
&lt;p&gt;А фронтенд, на основе JSX, поверх этого контента рендерил интерактивные элементы. Так как серверные и клиентские шаблоны шаблоны были написаны на разных языках, разработчики повторяли приблизительно один и тот же HTML в Mako и в JSX. И, &lt;em&gt;конечно же&lt;&#x2F;em&gt;, когда правили шаблоны на одном языке, часто забывали повторить изменения на другом&lt;&#x2F;p&gt;
&lt;p&gt;Проблема не то, чтобы критическая — небольшой content flash, для частного фикса которого надо всего-то немного покопипастить. Но она генерировала достаточное количество баг-репортов, чтобы показаться заслуживающей более основательного решения&lt;&#x2F;p&gt;
&lt;p&gt;Для SSR-инфраструктуры на NodeJS’е, на тот момент, ещё не созрели. Так что решили попробовать другой подход&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;план&quot;&gt;План&lt;&#x2F;h2&gt;
&lt;p&gt;Мы подумали: а что если вместо разворачивания NodeJS-серверов, попробуем писать интерактивные элементы на общем языке, понятном и на существующем бекенде, и на фронтенде? Тогда обе части стека смогут использовать одни и те же исходники шаблонов. Общие исходники -&amp;gt; ничего не надо повторять вручную -&amp;gt; меньше однообразных баг-репортов&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-python&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;def&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;render_navigation&lt;&#x2F;span&gt;():
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; (
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;div data-nav&amp;gt;&quot;&lt;&#x2F;span&gt; +
        render(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;navigation.tmpl&quot;&lt;&#x2F;span&gt;) +
        &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;&#x2F;div&amp;gt;&quot;&lt;&#x2F;span&gt;
    )
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;import&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;Nav&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;from&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;navigation.tmpl&quot;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;hydrateNavigation&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;for&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;let&lt;&#x2F;span&gt; n &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;of&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;document&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;querySelectorAll&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;[data-nav]&quot;&lt;&#x2F;span&gt;)) {
    &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ReactDOM&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;hydrate&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;language-xml&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;Nav&lt;&#x2F;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;, n)
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;За общий язык шаблонов выбрали &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;mustache.github.io&#x2F;&quot;&gt;&lt;code&gt;mustache&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. Модули для рендера &lt;code&gt;mustache&lt;&#x2F;code&gt;-шаблонов написаны на куче языков (а то и по несколько раз), а сами шаблоны — простые как пробка. В них нельзя описать логику на другом языке (в отличии от, к примеру, JSX, в котором можно писать вставки со сложными JS-выражениями). Плюс, язык шаблонов довольно подробно описан в &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;mustache.github.io&#x2F;mustache.5.html&quot;&gt;короткой спецификации&lt;&#x2F;a&gt;, с примерами и на человеческом языке&lt;&#x2F;p&gt;
&lt;p&gt;Несмотря на наличие рендеров на куче языков, они все отдают результат в виде обычной строки. &lt;em&gt;Нам&lt;&#x2F;em&gt; же, в идеале, надо рендерить шаблоны в React-код, чтобы не рисковать XSS из-за &lt;code&gt;dangerouslySetInnerHTML&lt;&#x2F;code&gt; и чтобы не терять оптимизаций, связанных с виртуальным DOM&lt;&#x2F;p&gt;
&lt;p&gt;Рендерить шаблоны в код решили с помощью кастомного webpack loader’а. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;webpack.js.org&#x2F;loaders&#x2F;&quot;&gt;Лоадеры&lt;&#x2F;a&gt; уже использовались на проекте для штук вроде импорта CSS-файлов, так что добавление ещё одного не станет проблемой&lt;&#x2F;p&gt;
&lt;p&gt;В итоге, мы хотели попробовать написать такой лоадер, который компилировал &lt;code&gt;mustache&lt;&#x2F;code&gt;-шаблоны в эквивалентный React-компонент. За &lt;code&gt;target&lt;&#x2F;code&gt; выбрали ES3 с вызовами &lt;code&gt;React.createElement&lt;&#x2F;code&gt;, чтобы избежать дополнительной компиляции бабелем:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; &#x60;&amp;lt;a href&#x3D;{{ href }}&amp;gt;{{ label }}&amp;lt;&#x2F;a&amp;gt;&#x60;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;var&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;require&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;react&quot;&lt;&#x2F;span&gt;)

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;module&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;exports&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-params&quot;&gt;props&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createElement&lt;&#x2F;span&gt;(
    &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;a&quot;&lt;&#x2F;span&gt;, { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;href&lt;&#x2F;span&gt;: props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;href&lt;&#x2F;span&gt; }, props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;label&lt;&#x2F;span&gt;
  )
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;И написали. Обозвали &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&quot;&gt;&lt;code&gt;schwartzman&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;ом (как актёра), залили на гитхаб и использовали в продакшене&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Прежде, чем продолжать… Да, это странное и, в каком-то смысле, overcomplicated решение проблемы SSR. Но это решение:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;не станет &lt;em&gt;проблемой&lt;&#x2F;em&gt;, когда появится NodeJS-инфраструктура,&lt;&#x2F;li&gt;
&lt;li&gt;не потребует переписывания всего стека (новый язык шаблонов дружит с Mako на бекенде и с JSX на фронтенде)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Да и я не надеюсь что вы будете использовать лоадер у себя. Даже не так. Надеюсь, что вы &lt;em&gt;не будете&lt;&#x2F;em&gt; использовать лоадер у себя&lt;&#x2F;p&gt;
&lt;p&gt;Ладно, адресовав возможные “ты говно и идея твоя говно”…&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id&#x3D;&quot;парсинг&quot;&gt;Парсинг&lt;&#x2F;h2&gt;
&lt;p&gt;Изнутри, как и многие компиляторы, &lt;code&gt;schwartzman&lt;&#x2F;code&gt; работает в два этапа: “парсинг” и “генерация кода”. На первом этапе, лоадер преобразовывает строку с исходными кодом в объект с синтаксическим деревом&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;regex&quot;&gt;Regex&lt;&#x2F;h3&gt;
&lt;p&gt;Первым делом, при написании парсера, хочется воспользоваться регулярными выражениями. Но это плохая идея по двум причинам:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;у нас слишком много правил для того, чтобы итоговая регулярка была читаемой&lt;&#x2F;li&gt;
&lt;li&gt;нам надо парсить HTML. Я не буду вдаваться в подробности почему “парсить HTML регулярками” — это плохая идея; в интернете &lt;a href&#x3D;&quot;http:&#x2F;&#x2F;htmlparsing.com&#x2F;regexes.html&quot;&gt;достаточно объяснений&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id&#x3D;&quot;custom-lexer--parser&quot;&gt;Custom lexer + parser&lt;&#x2F;h3&gt;
&lt;p&gt;Можно написать кастомные лексер (который разбивает строку на подстроки) и парсер (который собирает синтаксическое дерево из этих подстрок), чтобы процесс был максимально оптимизированным под наш синтаксис. Но это сложнааа, лень, триггерит воспоминания об универе, да и довольно бесполезно, потому что есть…&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;parsing-expression-grammar&quot;&gt;Parsing expression grammar&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;ru.wikipedia.org&#x2F;wiki&#x2F;%D0%93%D1%80%D0%B0%D0%BC%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0,_%D1%80%D0%B0%D0%B7%D0%B1%D0%B8%D1%80%D0%B0%D1%8E%D1%89%D0%B0%D1%8F_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5&quot;&gt;“Грамматика, разбирающая выражения”&lt;&#x2F;a&gt; или &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Parsing_expression_grammar&quot;&gt;“PEG”&lt;&#x2F;a&gt;. Разработчик описывает синтаксис языка именованными правилами, из которых потом генерируется парсер этого самого языка&lt;&#x2F;p&gt;
&lt;p&gt;Например, так может выглядеть грамматика для подмножества HTML, в котором есть только два тега, &lt;code&gt;&amp;lt;i&amp;gt;&lt;&#x2F;code&gt; и &lt;code&gt;&amp;lt;b&amp;gt;&lt;&#x2F;code&gt;, и нет никаких атрибутов:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;grammar bitsy

node        &amp;lt;- tag &#x2F; text_node
tag         &amp;lt;- i_tag &#x2F; b_tag
i_tag       &amp;lt;- &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;i&amp;gt;&quot;&lt;&#x2F;span&gt; node* &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;&#x2F;i&amp;gt;&quot;&lt;&#x2F;span&gt;
b_tag       &amp;lt;- &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;b&amp;gt;&quot;&lt;&#x2F;span&gt; node* &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;&#x2F;b&amp;gt;&quot;&lt;&#x2F;span&gt;
text_node   &amp;lt;- [^&amp;lt;&amp;gt;]+
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Для генерации парсера из PEG-файла есть несколько библиотек&lt;&#x2F;p&gt;
&lt;p&gt;Самая “знаменитая”, на мой взгляд, это &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.antlr.org&quot;&gt;ANTLR&lt;&#x2F;a&gt;, она довольно зрелая, умеет генерировать грамматику в JS, но для работы требует Джаву…&lt;&#x2F;p&gt;
&lt;p&gt;Из библиотек на JS, в 2015 год выбор был из &lt;a href&#x3D;&quot;http:&#x2F;&#x2F;canopy.jcoglan.com&quot;&gt;Canopy&lt;&#x2F;a&gt; и &lt;em&gt;очень&lt;&#x2F;em&gt; оригинально обозванной &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;pegjs.org&quot;&gt;PEGjs&lt;&#x2F;a&gt;. Выбрал, в итоге, первую. Не помню, по какой причине&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:1&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:1&quot; rel&#x3D;&quot;footnote&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, но не столь важно…&lt;&#x2F;p&gt;
&lt;p&gt;Важно то, что определившись с модулем для парсинга PEG, я мог начать постепенно описывать &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;grammar.peg&quot;&gt;синтаксис&lt;&#x2F;a&gt; для “HTML с mustache-вставками”, начав с корня дерева и добавив&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;хелперы, которые очистят дерево от всякого мусора&lt;&#x2F;li&gt;
&lt;li&gt;новые типы нод, разделив их на “HTML-элементы”, “&lt;code&gt;mustache&lt;&#x2F;code&gt;-теги” и “plain text”&lt;&#x2F;li&gt;
&lt;li&gt;произвольные имена тегов и функцию для валидации их сбалансированного открытия&#x2F;закрытия, чтобы можно было сгенерировать валидные вызовы &lt;code&gt;React.createElement&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;прочий синтаксис…&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code&gt;grammar Schwartzman

root_node           &amp;lt;- expr_node* %strip_whitespaces
dom_node            &amp;lt;- (open_html_tag expr_node* close_html_tag) %validate &lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;&amp;lt;DOMNode&amp;gt;&lt;&#x2F;span&gt; &#x2F; contained_html_tag &lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;&amp;lt;DOMNode&amp;gt;&lt;&#x2F;span&gt; &#x2F; commented_html &lt;span class&#x3D;&quot;hljs-symbol&quot;&gt;&amp;lt;CommentedDOMNode&amp;gt;&lt;&#x2F;span&gt;
open_html_tag       &amp;lt;- &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;&quot;&lt;&#x2F;span&gt; whitespace? tag_name whitespace? attr&lt;span class&#x3D;&quot;hljs-variable&quot;&gt;s:dom_attr&lt;&#x2F;span&gt;* whitespace? &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;gt;&quot;&lt;&#x2F;span&gt;
contained_html_tag  &amp;lt;- &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;&quot;&lt;&#x2F;span&gt; whitespace? tag_name whitespace? attr&lt;span class&#x3D;&quot;hljs-variable&quot;&gt;s:dom_attr&lt;&#x2F;span&gt;* whitespace? &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&#x2F;&amp;gt;&quot;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;После того, как перечислил все кейсы, мог сгенерировать модуль с функцией &lt;code&gt;parse&lt;&#x2F;code&gt;, которая:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;словит ошибки в шаблоне и кинет относительно читабельное сообщение. Например, если разработчик не сбалансировал теги (то есть, закрыл родительский тег до того, как закрыл все дочерние)&lt;&#x2F;li&gt;
&lt;li&gt;если шаблон ок, то отдаст готовенькое синтаксическое дерево&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;require&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;.&#x2F;grammar.js&quot;&lt;&#x2F;span&gt;)
  .&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;parse&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;&amp;lt;b&amp;gt;hello, &amp;lt;i&amp;gt;world&amp;lt;&#x2F;i&amp;gt;!&amp;lt;&#x2F;b&amp;gt;&quot;&lt;&#x2F;span&gt;)

&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;* &#x3D;&amp;gt;
  TreeNode {
    text: &#39;&amp;lt;b&amp;gt;hello, &amp;lt;i&amp;gt;world&amp;lt;&#x2F;i&amp;gt;!&amp;lt;&#x2F;b&amp;gt;&#39;,
    offset: 0,
    elements:
     [ TreeNode { text: &#39;&amp;lt;b&amp;gt;&#39;, offset: 0, elements: [] },
       TreeNode {
         text: &#39;hello, &amp;lt;i&amp;gt;world&amp;lt;&#x2F;i&amp;gt;!&#39;,
         offset: 3,
         elements:
          [ TreeNode { text: &#39;hello, &#39;, offset: 3, elements: [Array] },
            TreeNode { text: &#39;&amp;lt;i&amp;gt;world&amp;lt;&#x2F;i&amp;gt;&#39;, offset: 10, elements: [Array] },
            TreeNode { text: &#39;!&#39;, offset: 22, elements: [Array] } ] },
       TreeNode { text: &#39;&amp;lt;&#x2F;b&amp;gt;&#39;, offset: 23, elements: [] } ] }
*&#x2F;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;генерация-кода&quot;&gt;Генерация кода&lt;&#x2F;h2&gt;
&lt;p&gt;Имея синтаксическое дерево, лоадер может приступить к генерации React-кода&lt;&#x2F;p&gt;
&lt;p&gt;Упрощённо, лоадер выглядит как-то так:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;loader&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;content&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; tree &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;parse&lt;&#x2F;span&gt;(content)
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { code } &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;compile&lt;&#x2F;span&gt;(tree)

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;var h &#x3D; require(&quot;react&quot;).createElement

  module.exports &#x3D; function (props) { return &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${code}&lt;&#x2F;span&gt; }

  if (
    typeof process !&#x3D;&#x3D; &quot;undefined&quot;
    &amp;amp;&amp;amp; process.env
    &amp;amp;&amp;amp; process.env.NODE_ENV &#x3D;&#x3D;&#x3D; &quot;test&quot;
  ) { &#x2F;* … *&#x2F; }&#x60;&lt;&#x2F;span&gt;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Лоадер получает содержимое шаблона как строку, парсит его в синтаксическое дерево, передаёт получившееся дерево в функцию &lt;code&gt;compile&lt;&#x2F;code&gt;,  которая сгенерирует код компонента&lt;&#x2F;p&gt;
&lt;p&gt;Получившийся код компонента заворачивается в бойлерплейт с экспортом, дебаг-информацией для тестирования, и коротким именем для &lt;code&gt;createElement&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Внутри &lt;code&gt;compile&lt;&#x2F;code&gt;, лоадер рекурсивно проходится по синтаксическому дереву и возвращает объект с кодом и некоторой дополнительной информацией про контекст:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;compile&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;node&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; { tag_name, attrs, elements } &#x3D; node

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; children &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;compileChildren&lt;&#x2F;span&gt;(elements)
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; compiledAttrs &#x3D; &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;compileAttrs&lt;&#x2F;span&gt;(attrs)

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; {
    &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;code&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;h(&quot;&lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${tag_name}&lt;&#x2F;span&gt;&quot;, &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${compiledAttrs}&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-subst&quot;&gt;${children}&lt;&#x2F;span&gt;)&#x60;&lt;&#x2F;span&gt;,
    &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; …&lt;&#x2F;span&gt;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;schwartzman.js#L515-L554&quot;&gt;Реальный код сложнее&lt;&#x2F;a&gt;, так как нам надо обрабатывать кучу ограничений React’а и mustache’а&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;escaping&quot;&gt;Escaping&lt;&#x2F;h3&gt;
&lt;p&gt;Например, в &lt;code&gt;mustache&lt;&#x2F;code&gt; есть “обычная” замена переменной, которую можно скомпилировать в приблизительно такой React-код. По спецификации, HTML эскейпится при “обычной” замене, что совпадает со стандартным поведением &lt;code&gt;children&lt;&#x2F;code&gt;ов в React’е&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; &#x60;&amp;lt;div&amp;gt;{{ mark }}&amp;lt;&#x2F;div&amp;gt;&#x60;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createElement&lt;&#x2F;span&gt;(
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;div&quot;&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;, props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;mark&lt;&#x2F;span&gt;
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Для замены без эскейпинга, в &lt;code&gt;mustache&lt;&#x2F;code&gt; есть специальные теги &lt;code&gt;{{&amp;amp;&lt;&#x2F;code&gt; и &lt;code&gt;{{{&lt;&#x2F;code&gt;. Сами по себе, эти теги не вызывают проблем — просто генерируем &lt;em&gt;больше&lt;&#x2F;em&gt; кода и делов-то &lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; &#x60;&amp;lt;div&amp;gt;{{&amp;amp; mark }}&amp;lt;&#x2F;div&amp;gt;&#x60;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createElement&lt;&#x2F;span&gt;(
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;div&quot;&lt;&#x2F;span&gt;, {
    &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;dangerouslySetInnerHTML&lt;&#x2F;span&gt;: {
      &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;__html&lt;&#x2F;span&gt;: props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;mark&lt;&#x2F;span&gt;
    }
  }
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Но в Реакте, &lt;code&gt;dangerouslySetInnerHTML&lt;&#x2F;code&gt; — это свойство элемента (а не его дочерних нод), так что как только в шаблонах начинают комбинироваться теги без эскейпинга с любыми другими тегами или нодами, то лоадер должен это как-то обрабатывать:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-mustache&quot;&gt;&amp;lt;div&amp;gt;
  &amp;lt;i&amp;gt;{{ john }}:&amp;lt;&#x2F;i&amp;gt;
  oh, hi {{&amp;amp; mark }}
&amp;lt;&#x2F;div&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Можно было бы эскейпить соседние ноды “руками”. Но это сложнааа, потому что тогда надо перегенерировать код всей родительской ноды и её заэскейпленных &lt;code&gt;children&lt;&#x2F;code&gt;ов&lt;&#x2F;p&gt;
&lt;p&gt;Так что &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;schwartzman.js#L276-L289&quot;&gt;остановился&lt;&#x2F;a&gt; на другом варианте и ограничил количество дочерних нод, если хотя бы одна из них заэскейпленная. Всё равно отключение эскейпинга не поощряется…&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (children.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;length&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-number&quot;&gt;1&lt;&#x2F;span&gt; &amp;amp;&amp;amp; children[&lt;span class&#x3D;&quot;hljs-number&quot;&gt;0&lt;&#x2F;span&gt;].&lt;span class&#x3D;&quot;hljs-property&quot;&gt;escaped&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; add some &#x60;danger&#x60;&lt;&#x2F;span&gt;
} &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;else&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (children.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;some&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-function&quot;&gt;&lt;span class&#x3D;&quot;hljs-params&quot;&gt;c&lt;&#x2F;span&gt; &#x3D;&amp;gt;&lt;&#x2F;span&gt; c.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;escaped&lt;&#x2F;span&gt;)) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;throw&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;new&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;OneChildPolicy&lt;&#x2F;span&gt;()
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id&#x3D;&quot;-section-&quot;&gt;&lt;code&gt;{{# section }}&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Но не только &lt;code&gt;react&lt;&#x2F;code&gt; усложняет написание лоадера, но и сам &lt;code&gt;mustache&lt;&#x2F;code&gt;…&lt;&#x2F;p&gt;
&lt;p&gt;Несмотря на то, что &lt;code&gt;mustache&lt;&#x2F;code&gt; называет себя “logic-less”, в нём есть пара тегов, которые по тому или иному условию рендерят своё тело&lt;&#x2F;p&gt;
&lt;p&gt;Первый, invert, работает как &lt;code&gt;if not&lt;&#x2F;code&gt; — тело тега рендерится, если значение в контексте либо &lt;code&gt;falsey&lt;&#x2F;code&gt;, либо пустой массив. Для этого тега несложно сгенерировать код, который будет отдавать правильный контент в зависимости от значений в &lt;code&gt;props&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;&#x2F; &#x60;&amp;lt;b&amp;gt;{{^ great }}Not terrible{{&#x2F; great }}&amp;lt;&#x2F;b&amp;gt;&#x60;&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createElement&lt;&#x2F;span&gt;(
  &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;b&quot;&lt;&#x2F;span&gt;,
  &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;,
  (!props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;great&lt;&#x2F;span&gt; || !props.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;great&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;length&lt;&#x2F;span&gt;)
    ? &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;Not terrible&quot;&lt;&#x2F;span&gt;
    : &lt;span class&#x3D;&quot;hljs-literal&quot;&gt;null&lt;&#x2F;span&gt;
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Второй же “условный” тег, также известный как “секция”, выполняет кучу ролей, каждая из которых зависит от значений в контексте. Например, для шаблона &lt;code&gt;&amp;lt;div&amp;gt;{{# user }}{{ name }}{{&#x2F; user }}&amp;lt;&#x2F;div&amp;gt;&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Если &lt;code&gt;user&lt;&#x2F;code&gt; — это булевое значение, то секция работает как &lt;code&gt;if&lt;&#x2F;code&gt;, а шаблон можно скомпилировать в элемент &lt;code&gt;React.createElement(&quot;div&quot;, null, props.user ? props.name : null)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Если &lt;code&gt;user&lt;&#x2F;code&gt; — это объект, то секция работает как &lt;code&gt;scope&lt;&#x2F;code&gt; (или &lt;code&gt;namespace&lt;&#x2F;code&gt;), “ограничивая” доступ к &lt;code&gt;props&lt;&#x2F;code&gt;. В итоге, шаблон можно скомпилировать в элемент &lt;code&gt;React.createElement(&quot;div&quot;, null, props.user ? props.user.name : null)&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Только в рантайме компонент может знать, откуда доставать &lt;code&gt;name&lt;&#x2F;code&gt; — из корня &lt;code&gt;props&lt;&#x2F;code&gt; или из вложенного объекта &lt;code&gt;user&lt;&#x2F;code&gt;…&lt;&#x2F;p&gt;
&lt;p&gt;Так что лоадер, кроме вызовов &lt;code&gt;React.createElement&lt;&#x2F;code&gt;, также должен добавить в код рантайм-хелперы&lt;&#x2F;p&gt;
&lt;p&gt;Один из таких хелперов — это &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;schwartzman.js#L323-L338&quot;&gt;функция для поиска значения в контексте&lt;&#x2F;a&gt;, которой передаётся список возможных “скоупов” (в нашем случае это &lt;code&gt;[&quot;user&quot;]&lt;&#x2F;code&gt;), и ключ, который нужно найти (&lt;code&gt;name&lt;&#x2F;code&gt;). Если же ключа нет, то, по спецификации, заменяем тег на пустое значение&lt;&#x2F;p&gt;
&lt;p&gt;Другой хелпер помогает с двумя другими “ролями” секции:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Если в &lt;code&gt;props&lt;&#x2F;code&gt; передан массив, то секция работает как &lt;code&gt;for_each&lt;&#x2F;code&gt; и повторяет своё тело для каждого элемента этого массива&lt;&#x2F;li&gt;
&lt;li&gt;Если в &lt;code&gt;props&lt;&#x2F;code&gt; передана функция, то секция работает как &lt;code&gt;lambda&lt;&#x2F;code&gt; и рендерер заменят тело секции результатом вызова функции&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Логика для проверки “роли” секции становится слишком сложной, так что выносим его в &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;schwartzman.js#L363-L378&quot;&gt;хелпер-функцию &lt;code&gt;section&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. Она находит значение секции в &lt;code&gt;props&lt;&#x2F;code&gt; и, в зависимости от его типа, отдаёт соответсвующее React-дерево:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;function section(&#x2F;* … *&#x2F;) {
  var obj &#x3D; scopeSearch(&#x2F;* scope section *&#x2F;)

  if (obj) {
    if (isArray(obj)) {
      &#x2F;&#x2F; for_each section
    } else if (isFunction(obj)) {
      &#x2F;&#x2F; lambda section
    } else {
      &#x2F;&#x2F; if section
    }
  }
}&#x60;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Самое интересное начинается, когда секция выполняет роль “лямбды”. Потому что, по спецификации &lt;code&gt;mustache&lt;&#x2F;code&gt;, функция в такой секции:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;может вернуть строку с HTML&lt;&#x2F;li&gt;
&lt;li&gt;принимает в аргументах тело секции и функцию для рендера своего тела с правильными скоупами переменных&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Так что нам нужен волшебный хелпер, который в рантайме из HTML-строки сгенерирует React-дерево. &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;src&#x2F;schwartzman.js#L345-L361&quot;&gt;Этим хелпером&lt;&#x2F;a&gt; является сам &lt;code&gt;schwartzman&lt;&#x2F;code&gt;. Так что лоадер, при генерации кода, должен включить сам себя в результат, чтобы React-компонент мог за&lt;code&gt;eval&lt;&#x2F;code&gt;ить&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:2&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:2&quot; rel&#x3D;&quot;footnote&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; результат и отдать другой, скомпилированный в рантайме, компонент:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;function render(&#x2F;* … *&#x2F;) {
  var lowLevel &#x3D; require(&quot;schwartzman&quot;).lowLevel
  var parsed &#x3D; lowLevel.parse(&#x2F;* … *&#x2F;)
  &#x2F;&#x2F; …
  return eval(lowLevel.compile(parsed))
}&#x60;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id&#x3D;&quot;удобства&quot;&gt;Удобства&lt;&#x2F;h2&gt;
&lt;p&gt;Ограничения &lt;code&gt;mustache&lt;&#x2F;code&gt; влияют не только на то, что мы &lt;em&gt;должны&lt;&#x2F;em&gt; делать при обработке секций, но и на то, что &lt;em&gt;можно&lt;&#x2F;em&gt; делать. Например, нельзя расширить синтаксис штуками вроде явно именованных элементов массивов:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class&#x3D;&quot;hljs-template-tag&quot;&gt;{{#&lt;span class&#x3D;&quot;hljs-name&quot;&gt;users&lt;&#x2F;span&gt; u}}&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;language-xml&quot;&gt;
  &lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-template-variable&quot;&gt;{{ &lt;span class&#x3D;&quot;hljs-name&quot;&gt;u.name&lt;&#x2F;span&gt; }}&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;language-xml&quot;&gt;
&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-template-tag&quot;&gt;{{&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;users&lt;&#x2F;span&gt;}}&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Нам надо соблюдать совместимость с &lt;code&gt;mustache&lt;&#x2F;code&gt;м на бекенде, а он не знает ничего про особую роль пробелов в названии секции&lt;&#x2F;p&gt;
&lt;p&gt;В первых версиях лоадера, до меня это не дошло, так что я попытался добавить &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;commit&#x2F;1ec78746b474cae4306f42e9a12307aa7308adc3&quot;&gt;поддержку именованных элементов массива&lt;&#x2F;a&gt;. Только чтобы &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;commit&#x2F;71c85000e93bd76c574f32aed37f0ce4dd7d620c&quot;&gt;откатить всё обратно&lt;&#x2F;a&gt;, когда renderer &lt;code&gt;mustache&lt;&#x2F;code&gt;а на бекенде не мог найти непонятные ключи в контексте&lt;&#x2F;p&gt;
&lt;p&gt;Хотя, это и хорошо, что необходимость совместимости не пускала сильно далеко. Потому что вместо траты времени и усилий на разработку и поддержку неважных фич, можно было вкладываться в удобство использования лоадером&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;demo-страница&quot;&gt;Demo-страница&lt;&#x2F;h3&gt;
&lt;p&gt;Одним из таких удобств была &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;schwartzman.anton.codes&#x2F;&quot;&gt;demo-страничка&lt;&#x2F;a&gt;, на которой можно вбить шаблон и контекст, и получить отрендеренный компонент с его кодом. Идея, конечно, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;babeljs.io&#x2F;repl&quot;&gt;далеко&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;rollupjs.org&#x2F;repl&#x2F;&quot;&gt;не&lt;&#x2F;a&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.typescriptlang.org&#x2F;play&#x2F;index.html&quot;&gt;оригинальная&lt;&#x2F;a&gt;, но всё ещё недостаточно распространённая, как на мой взгляд&lt;&#x2F;p&gt;
&lt;p&gt;Сначала мне казалось, что сделать такую страничку будет сложно из-за того, что надо будет возиться с зависимостями и с отличиями между окружениями NodeJS (в котором лоадер крутился до этого) и браузера (в котором будет крутиться демо-страничка)&lt;&#x2F;p&gt;
&lt;p&gt;Но, когда начал перечислять все зависимости, всё оказалось намного проще:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Из пакетов:&lt;ul&gt;
&lt;li&gt;сгенерированный код зависит только от React’а (и то опционально) и от самого лоадера для обработки &lt;code&gt;{{# lambda }}&lt;&#x2F;code&gt;-секций&lt;&#x2F;li&gt;
&lt;li&gt;а лоадер — только от функции для парсинга вебпаковских настроек&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Про ноду, лоадер и сгенерированных код знают очень мало:&lt;ul&gt;
&lt;li&gt;что зависимости получаются вызовом функции &lt;code&gt;require&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;и что в окружении есть &lt;code&gt;module.exports&lt;&#x2F;code&gt; и &lt;code&gt;process.env.NODE_ENV&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;var&lt;&#x2F;span&gt; h &#x3D; &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;require&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;react&quot;&lt;&#x2F;span&gt;).&lt;span class&#x3D;&quot;hljs-property&quot;&gt;createElement&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;var&lt;&#x2F;span&gt; lowLevel &#x3D; &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;require&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;schwartzman&quot;&lt;&#x2F;span&gt;).&lt;span class&#x3D;&quot;hljs-property&quot;&gt;lowLevel&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;var&lt;&#x2F;span&gt; parseQuery &#x3D; &lt;span class&#x3D;&quot;hljs-built_in&quot;&gt;require&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;loader-utils&quot;&lt;&#x2F;span&gt;).&lt;span class&#x3D;&quot;hljs-property&quot;&gt;parseQuery&lt;&#x2F;span&gt;

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;module&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;exports&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-params&quot;&gt;content&lt;&#x2F;span&gt;) { &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;* … *&#x2F;&lt;&#x2F;span&gt; }

&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;typeof&lt;&#x2F;span&gt; process !&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;undefined&quot;&lt;&#x2F;span&gt;
  &amp;amp;&amp;amp; process.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;env&lt;&#x2F;span&gt;
  &amp;amp;&amp;amp; process.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;env&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;NODE_ENV&lt;&#x2F;span&gt; &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;test&quot;&lt;&#x2F;span&gt;
) { &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;* … *&#x2F;&lt;&#x2F;span&gt; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Все эти зависимости &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;docs&#x2F;index.html#L57-L71&quot;&gt;легко замокать вручную&lt;&#x2F;a&gt;, избежав возни с бандлерами и дополнительным &lt;code&gt;build&lt;&#x2F;code&gt;-степом для демо-страницы:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;window&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;module&lt;&#x2F;span&gt; &#x3D; {}
&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;window&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;process&lt;&#x2F;span&gt; &#x3D; { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;env&lt;&#x2F;span&gt;: { &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;NODE_ENV&lt;&#x2F;span&gt;: &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;test&quot;&lt;&#x2F;span&gt; } }

&lt;span class&#x3D;&quot;hljs-variable language_&quot;&gt;window&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;require&lt;&#x2F;span&gt; &#x3D; &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;function&lt;&#x2F;span&gt; (&lt;span class&#x3D;&quot;hljs-params&quot;&gt;name&lt;&#x2F;span&gt;) {
  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (name &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;loader-utils&quot;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; { &lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;parseQuery&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-params&quot;&gt;&lt;&#x2F;span&gt;) { &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; {} } } 
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (name &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;react&quot;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;*
      &amp;lt;script src&#x3D;&quot;https:&#x2F;&#x2F;unpkg.com&#x2F;react@16&#x2F;umd&#x2F;react.production.min.js&quot;&amp;gt;&amp;lt;&#x2F;script&amp;gt;
    *&#x2F;&lt;&#x2F;span&gt;
  }

  &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;if&lt;&#x2F;span&gt; (name &#x3D;&#x3D;&#x3D; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;schwartzman&quot;&lt;&#x2F;span&gt;) {
    &lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;return&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&#x2F;*
      &amp;lt;script src&#x3D;&quot;https:&#x2F;&#x2F;unpkg.com&#x2F;schwartzman&quot;&amp;gt;&amp;lt;&#x2F;script&amp;gt;
    *&#x2F;&lt;&#x2F;span&gt;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Причём, &lt;code&gt;parseQuery&lt;&#x2F;code&gt; (функцию для парсинга вебпаковских опций) можно полностью заменить на свою реализацию, которая всегда будет отдавать одно и то же. А реакт и лоадер можно тянуть с &lt;code&gt;unpkg&lt;&#x2F;code&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Существование этой странички &lt;em&gt;очень&lt;&#x2F;em&gt; помогло с баг-репортами, особенно когда начал прописывать входные шаблон и &lt;code&gt;props&lt;&#x2F;code&gt; в URL&lt;&#x2F;p&gt;
&lt;p&gt;Так что если лоадер падает или странно рендерит шаблон, его пользователи &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;issues&#x2F;3&quot;&gt;могли кинуть ссылку&lt;&#x2F;a&gt; на проблемный шаблон в Slack или Github Issues. Так что и для юзеров есть чёткий flow для жалоб, и я могу сэкономить время на написание тест-кейсов. Так что да, &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v&#x3D;PvM79DJ2PmM&quot;&gt;чем меньше модуль знает, тем лучше&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h3 id&#x3D;&quot;тестирование-зависимостей&quot;&gt;Тестирование зависимостей&lt;&#x2F;h3&gt;
&lt;p&gt;Прошло два года. Лоадер продолжал использоваться то тут, то там, так что я добавляю тесты на сгенерированный код и отрендеренный им статический маркап, плюс периодически проверяю совместимость с новыми версиями Реакта&lt;&#x2F;p&gt;
&lt;p&gt;Переход на пятнадцатую версию не вызвал никаких проблем, а вот с выходом шестнадцатой билд становится красным…&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-html&quot;&gt;&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;div&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;style&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;color: red;&quot;&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;hello world&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;div&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
            &lt;span class&#x3D;&quot;hljs-comment&quot;&gt;&amp;lt;!--  ^   ^ --&amp;gt;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;div&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;style&lt;&#x2F;span&gt;&#x3D;&lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;color:red&quot;&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;hello world&lt;span class&#x3D;&quot;hljs-tag&quot;&gt;&amp;lt;&#x2F;&lt;span class&#x3D;&quot;hljs-name&quot;&gt;div&lt;&#x2F;span&gt;&amp;gt;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Дело в том, что шестнадцатый &lt;code&gt;ReactDOM&lt;&#x2F;code&gt; поменял то, как форматируются inline-стили, так что тесты на отрендеренный шаблон начали падать. Передо мной предстал выбор:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Либо&lt;&#x2F;strong&gt; переписать тесты на шестнадцатый React&lt;&#x2F;p&gt;
&lt;p&gt;Для лоадера с малым количеством кода и достаточным количеством тестов это довольно быстро, но, по сути, сделает лоадер совместимым только с последней версией React’а. Потому что вдруг что-то поломается в совместимости с прошлой версией, а помнить проверять всё ручками я не могу&lt;&#x2F;p&gt;
&lt;p&gt;Для пользователей же лоадера, обновление их основной UI-библиотеки — дело &lt;em&gt;намного&lt;&#x2F;em&gt; более муторное&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Лиииибо&lt;&#x2F;strong&gt; я мог бы добавить в тесты проверки на версию React’а и ожидать соответствующие результаты в тест-кейсах&lt;&#x2F;p&gt;
&lt;p&gt;Пользователям это дико упрощает миграцию — они могут обновлять React и лоадер без привязки друг к другу&lt;&#x2F;p&gt;
&lt;p&gt;Мне же нужно описывать отдельные тест-кейсы для разных React’ов и как-то устанавливать с &lt;code&gt;npm&lt;&#x2F;code&gt;а правильные версии зависимостей перед прогоном тестов, а это сложнааа. Но, так как я уже дважды выбирал простой путь, так что пора выбрать и сложный…&lt;&#x2F;p&gt;
&lt;p&gt;Поэтому пишем в тестах &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;master&#x2F;test&#x2F;rendering.test.js#L204-L223&quot;&gt;ожидания, зависящие от версии&lt;&#x2F;a&gt; React’а&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:3&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:3&quot; rel&#x3D;&quot;footnote&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-js&quot;&gt;&lt;span class&#x3D;&quot;hljs-keyword&quot;&gt;const&lt;&#x2F;span&gt; rendered &#x3D; semver.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;gte&lt;&#x2F;span&gt;(&lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-property&quot;&gt;version&lt;&#x2F;span&gt;, &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;16.0.0&quot;&lt;&#x2F;span&gt;)
  ? &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;div style&#x3D;&quot;color:red&quot;&amp;gt;red&amp;lt;&#x2F;div&amp;gt;&#x60;&lt;&#x2F;span&gt;
  : &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&#x60;&amp;lt;div style&#x3D;&quot;color:red;&quot;&amp;gt;red&amp;lt;&#x2F;div&amp;gt;&#x60;&lt;&#x2F;span&gt;

assert.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;equal&lt;&#x2F;span&gt;(
  rendered,
  &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;ReactDOMServer&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;renderToStaticMarkup&lt;&#x2F;span&gt;(
    &lt;span class&#x3D;&quot;hljs-title class_&quot;&gt;React&lt;&#x2F;span&gt;.&lt;span class&#x3D;&quot;hljs-title function_&quot;&gt;createElement&lt;&#x2F;span&gt;(tmpl, {})
  )
)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;В &lt;code&gt;package.json&lt;&#x2F;code&gt; &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;package.json#L41-L43&quot;&gt;описываем&lt;&#x2F;a&gt;, какая версия React’а может быть у пользователя лоадера. В нашем случае, это версии из трёх “веток” — 14, 15 и 16. Делаем это в &lt;code&gt;peerDependencies&lt;&#x2F;code&gt;, а не в &lt;code&gt;dependencies&lt;&#x2F;code&gt; для того, чтобы оставить пользователям возможность выбора версии React’а&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:4&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:4&quot; rel&#x3D;&quot;footnote&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-json&quot;&gt;&lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;peerDependencies&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;{&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-attr&quot;&gt;&quot;react&quot;&lt;&#x2F;span&gt;&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;:&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;^0.14.3 || ^15.6.1 || ^16.0.0&quot;&lt;&#x2F;span&gt;
&lt;span class&#x3D;&quot;hljs-punctuation&quot;&gt;}&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Теперь нам надо подготовить CI окружение (в случае &lt;code&gt;schwartzman&lt;&#x2F;code&gt;, это &lt;em&gt;был&lt;&#x2F;em&gt;&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:gh-actions&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:gh-actions&quot; rel&#x3D;&quot;footnote&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; Travis CI) для прогона тестов. Для этого описываем в конфиге &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;.travis.yml#L11-L16&quot;&gt;переменную окружения&lt;&#x2F;a&gt; с версиями React’а, против которых гонять тесты&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-yaml&quot;&gt;&lt;span class&#x3D;&quot;hljs-attr&quot;&gt;env:&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;REACT_VERSION&#x3D;*&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;REACT_VERSION&#x3D;0.14.3&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;REACT_VERSION&#x3D;15.6.1&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;REACT_VERSION&#x3D;16.0.0&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;И там же &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;da03843b4b50512f69c673d7a3d20125e7b5435c&#x2F;.travis.yml#L8-L10&quot;&gt;правим&lt;&#x2F;a&gt; &lt;code&gt;install&lt;&#x2F;code&gt;-шаг так, чтобы после стандартной установки зависимостей, устанавливались &lt;code&gt;react&lt;&#x2F;code&gt; и &lt;code&gt;react-dom&lt;&#x2F;code&gt; необходимых версий:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code class&#x3D;&quot;language-yaml&quot;&gt;&lt;span class&#x3D;&quot;hljs-attr&quot;&gt;install:&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;npm&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;ci&lt;&#x2F;span&gt;
  &lt;span class&#x3D;&quot;hljs-bullet&quot;&gt;-&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;npm&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;i&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;--no-save&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;react@$REACT_VERSION&quot;&lt;&#x2F;span&gt; &lt;span class&#x3D;&quot;hljs-string&quot;&gt;&quot;react-dom@$REACT_VERSION&quot;&lt;&#x2F;span&gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;И готово. Не так уж и сложнааа, но теперь тесты словят меня, если&#x2F;когда я &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;travis-ci.org&#x2F;zemlanin&#x2F;schwartzman&#x2F;builds&#x2F;436183446&quot;&gt;поломаю совместимость с прошлыми версиями React’а&lt;&#x2F;a&gt;. А пользователи лоадера смогут получить оптимизации новых версий без необходимости переписывать весь остальной код на новую версию React’а&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src&#x3D;&quot;https:&#x2F;&#x2F;zemlan.in&#x2F;media&#x2F;DbVTLblpI8ugJLGDSp8U09zl9A&#x2F;fit1600.png&quot; alt&#x3D;&quot;&quot;&gt;&lt;&#x2F;p&gt;
&lt;h2 id&#x3D;&quot;опыт&quot;&gt;Опыт&lt;&#x2F;h2&gt;
&lt;p&gt;Лоадер несколько лет использовался, чтобы рендерить всякую мелочь в проекте. В конце восемнадцатого-начале девятнадцатого года, подняли-таки инфраструктуру для SSR, так что &lt;code&gt;schwartzman&lt;&#x2F;code&gt;а удалили из зависимостей… Но, несмотря на то, что лоадер скорее мёртв, приобретенный опыт продолжает жить&lt;&#x2F;p&gt;
&lt;p&gt;Опыт, выражающийся в понимании, что чем меньше код знает о внешнем мире, тем лучше&lt;&#x2F;p&gt;
&lt;p&gt;В знании, что с грамотной спецификацией, задачу можно перенести в контекст, про который её авторы даже не подозревали&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:5&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:5&quot; rel&#x3D;&quot;footnote&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;p&gt;В понимании, что система импортов — это не что-то “данное сверху”, а ещё одно API, которое &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;twitter.zemlan.in&#x2F;zemlanin&#x2F;status&#x2F;1110303654354001921&quot;&gt;можно&lt;&#x2F;a&gt; подстроить &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;pigeon&#x2F;blob&#x2F;301e9a37c4fc2cad3eab9205653730d58e1f6166&#x2F;src&#x2F;server&#x2F;bundle.js&quot;&gt;под себя&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;И в знании, что тестировать можно не только текущие версии интеграции, но прошлые и будущие&lt;sup&gt;&lt;a href&#x3D;&quot;#fn:post-2019-12-6Xt5yJnLai:6&quot; id&#x3D;&quot;rfn:post-2019-12-6Xt5yJnLai:6&quot; rel&#x3D;&quot;footnote&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;&lt;&#x2F;p&gt;
&lt;div class&#x3D;&quot;footnotes&quot;&gt;&lt;hr&gt;&lt;ol&gt;&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:1&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Может, отпугнуло сообщение на сайте PEGjs, что библиотека “is very much in development”; может, хотелось иметь возможность сгенерировать парсер в Python…&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:1&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:2&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Как же приятно иметь оправдание для запрещённого приёма…&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:2&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:3&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Для проверки версии лучше не писать свой парсер semver’а, а использовать &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;semver&quot;&gt;уже существующий&lt;&#x2F;a&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:3&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:4&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Честно говоря, это делать необязательно, но желательно. Так они смогут сообщить, что лоадер мешает им обновиться, и попросят меня проверить совместимость с новой версией React’а&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:4&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:gh-actions&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;После публикации записи, перенёс CI на Github Actions и немного упростил &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;zemlanin&#x2F;schwartzman&#x2F;blob&#x2F;0b1c4c91bf053406a8bfb2478f4763d929bcf672&#x2F;.github&#x2F;workflows&#x2F;react-compat.yml&quot;&gt;конфиги для тестирования совместимости&lt;&#x2F;a&gt;&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:gh-actions&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:5&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Первые коммиты в &lt;code&gt;mustache&lt;&#x2F;code&gt; были задолго до публичного анонса Реакта, но благодаря подробному описанию и ограниченному синтаксису первого, подружить его со вторым было довольно легко&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:5&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id&#x3D;&quot;fn:post-2019-12-6Xt5yJnLai:6&quot; tabindex&#x3D;&quot;-1&quot;&gt;&lt;p&gt;Когда в Preact’е поломалась совместимость между версиями, можно было &lt;a href&#x3D;&quot;https:&#x2F;&#x2F;github.com&#x2F;developit&#x2F;preact-css-transition-group&#x2F;pull&#x2F;1&quot;&gt;не только починить её&lt;&#x2F;a&gt;, но и добавить тесты, чтобы предотвратить повторение этой проблемы в будущем&amp;nbsp;&lt;a href&#x3D;&quot;#rfn:post-2019-12-6Xt5yJnLai:6&quot; rev&#x3D;&quot;footnote&quot;&gt;↩︎&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;&lt;&#x2F;ol&gt;&lt;&#x2F;div&gt;
        
      </description>
      <link>https://zemlan.in/react-with-mustaches.html</link>
      <guid isPermaLink="false">post-2019-12-6Xt5yJnLai</guid>
      <pubDate>Thu, 19 Dec 2019 20:02:00 GMT</pubDate>
    </item>
</channel>
</rss>
