<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>OBISCR&apos;s Blog</title><description>A software engineer passionate about crafting high-performance solutions with expertise in TypeScript, Rust, and Go. Specializing in optimizing code for seamless user experiences, from efficient backend services to elegant frontend solutions. Dedicated to pushing the boundaries of software development. Let&apos;s collaborate and create something extraordinary!</description><link>https://obiscr.com</link><item><title>在线64卦变换表</title><link>https://obiscr.com/blog/64gua</link><guid isPermaLink="true">https://obiscr.com/blog/64gua</guid><description>在线版《东方桥先生64卦变化表》。支持选择高亮。根据上卦和下卦快速找到卦的详细信息。</description><content:encoded>&lt;p&gt;import { LinkCard, Aside, CardGrid, Card } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;近期重新制作了在线版的64卦变换表，效果如上图所示。建议先去体验一下。&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://64gua.online&quot;
title=&quot;64卦在线预览&quot;
description=&quot;在线版《东方桥先生64卦变化表》。支持选择高亮。根据上卦和下卦快速找到卦的详细信息。&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;点击第一列的任意一项可以选中当前行，点击行的任意一项可以选中当前列。两个交叉的位置就是得到的卦。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;position: relative; width: 100%; padding-bottom: 56.25%;&quot;&amp;gt;
&amp;lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113068923160062&amp;amp;bvid=BV1vDHyeAE2A&amp;amp;cid=25718489095&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;position: absolute; top: 0; left: 0; width: 100%; height: 100%;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;绘制SVG图标&lt;/h2&gt;
&lt;h3&gt;原理说明&lt;/h3&gt;
&lt;p&gt;每一个爻都有两种状态：&lt;code&gt;阳&lt;/code&gt; 或者 &lt;code&gt;阴&lt;/code&gt;。因此很自然可以想到用 0 和 1 来表示。基本卦有3爻，衍生卦有6爻。因此可以使用二进制数字来表示。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;例如&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;svg class=&quot;inline-block&quot; width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;&amp;lt;path d=&quot;M20 1.66669H0V5.00002H20V1.66669Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;path d=&quot;M8.33333 8.33331H0V11.6666H8.33333V8.33331Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;path d=&quot;M20 8.33331H11.6666V11.6666H20V8.33331Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;path d=&quot;M8.33333 15H0V18.3333H8.33333V15Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;path d=&quot;M20 15H11.6666V18.3333H20V15Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;/svg&amp;gt; (艮)：可以表示为 &lt;code&gt;100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;svg class=&quot;inline-block&quot; width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;&amp;lt;path d=&quot;M20 1.66669H0V5.00002H20V1.66669Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;path d=&quot;M20 8.33002H0V11.6634H20V8.33002Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;path d=&quot;M8.33333 15H0V18.3333H8.33333V15Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;path d=&quot;M20 15H11.6666V18.3333H20V15Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;/svg&amp;gt; (巽)：可以表示为 &lt;code&gt;110&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;svg class=&quot;inline-block&quot; width=&quot;20&quot; height=&quot;20&quot; viewBox=&quot;0 0 20 20&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;&amp;lt;path d=&quot;M20 1.66669H0V5.00002H20V1.66669Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;path d=&quot;M20 8.33002H0V11.6634H20V8.33002Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;path d=&quot;M20 15H0V18.3333H20V15Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;/svg&amp;gt; (乾)：可以表示为 &lt;code&gt;111&lt;/code&gt;
&amp;lt;/Aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;像素计算&lt;/h3&gt;
&lt;p&gt;首先要确定SVG的尺寸，对于8个基本卦，以 &lt;code&gt;宽:24&lt;/code&gt; / &lt;code&gt;高:24&lt;/code&gt; 来绘制。对于64个衍生卦，以 &lt;code&gt;宽:24&lt;/code&gt; / &lt;code&gt;高:48&lt;/code&gt; 来绘制。
下面将以衍生卦设计图来举例。&lt;/p&gt;
&lt;p&gt;我们以卦序第55卦&lt;code&gt;丰&lt;/code&gt;为例，详细解释一下二进制的转化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/55-feng.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在我们这套系统中，二进制的最高位（一共有六位，分别对应六爻）就是第六爻，依次类推，最低位就是第一爻。
因此，把丰卦转化为二进制就是 &lt;code&gt;001101&lt;/code&gt;。原理知道以后，需要计算具体的像素值。&lt;/p&gt;
&lt;p&gt;这是上面丰卦的设计草图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/skeleton-feng.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Card title=&quot;横向X轴&quot; icon=&quot;moon&quot;&amp;gt;
&amp;lt;Aside type=&quot;note&quot; title=&quot;总宽度：24像素&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;阳爻：直接是24px宽。阴爻：10 + 4 + 10 = 24;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;svg class=&quot;inline-block&quot; width=&quot;24&quot; height=&quot;4&quot; viewBox=&quot;0 0 24 4&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;&amp;lt;path d=&quot;M24 0H0V4H24V0Z&quot; fill=&quot;#FF0000&quot;/&amp;gt;&amp;lt;/svg&amp;gt;
如果是阳爻，就直接占满X轴整个区域。也就是宽度为24。&lt;/p&gt;
&lt;p&gt;&amp;lt;svg class=&quot;inline-block&quot; width=&quot;24&quot; height=&quot;4&quot; viewBox=&quot;0 0 24 4&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;&amp;lt;path d=&quot;M10 0H0V4H10V0Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;path d=&quot;M24 0H14V4H24V0Z&quot; fill=&quot;black&quot;/&amp;gt;&amp;lt;/svg&amp;gt;
如果是阴爻 ，
留出中间部分4个像素的空间。剩余20个像素，给左右两侧的半个爻各10个像素。刚好分配完横向的24个像素（10 + 4 + 10 = 24）。&lt;/p&gt;
&lt;p&gt;第二个方块的起始位置就是：第一个方块的宽度10 ➕ 4个像素的空白空间 = 14（下方代码第&lt;code&gt;30&lt;/code&gt;行）。
&amp;lt;/Card&amp;gt;
&amp;lt;Card title=&quot;纵向Y轴&quot; icon=&quot;sun&quot;&amp;gt;
&amp;lt;Aside type=&quot;note&quot; title=&quot;总高度：48像素&quot;&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2(顶部空间) + 4(每个爻高度) * 6(一共六个爻) + 5(6个爻之间的空间) * 4(每个空间的高度) + 2(底部空间) = 48; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/Aside&amp;gt;
纵向只需要累加Y轴的距离就可以，不用关心是阳还是阴爻。纵向一共有6爻，每个高度是4，一共是24。六个爻之间会有5个空白空间。每个空间是4像素高。
因此。需要注意刚开始第一爻的Y轴应该从2开始（下方代码第23, 31, 43行），然后每一次加8（上一爻和空白空间加起来），最终就绘制出Y轴的图形。
&amp;lt;/Card&amp;gt;&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;一切准备就绪之后， 最终代码实现就非常简单了。参数 base 为 true，表示绘制基本卦，否则绘制衍生卦。参数 value 是某个卦的二进制数值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const IChingIcon = ({ base, value }: IconProps) =&amp;gt; {
  const valueArray = binaryStringToArray(value);

  return (
    &amp;lt;svg
      key={value}
      fill=&quot;none&quot;
      height={base ? &quot;24&quot; : &quot;48&quot;}
      viewBox={base ? &quot;0 0 24 24&quot; : &quot;0 0 24 48&quot;}
      width=&quot;24&quot;
      xmlns=&quot;http://www.w3.org/2000/svg&quot;
    &amp;gt;
      {valueArray.map((value, index) =&amp;gt; {
        if (value === 0) {
          return (
            &amp;lt;React.Fragment key={index}&amp;gt;
              &amp;lt;rect
                key={`left-${index}`}
                fill=&quot;currentColor&quot;
                height=&quot;4&quot;
                width=&quot;10&quot;
                x=&quot;0&quot;
                y={8 * index + 2}
              /&amp;gt;
              &amp;lt;rect
                key={`right-${index}`}
                fill=&quot;currentColor&quot;
                height=&quot;4&quot;
                width=&quot;10&quot;
                x=&quot;14&quot;
                y={8 * index + 2}
              /&amp;gt;
            &amp;lt;/React.Fragment&amp;gt;
          );
        } else {
          return (
            &amp;lt;rect
              key={index}
              fill=&quot;#FF0000&quot;
              height=&quot;4&quot;
              width=&quot;24&quot;
              x=&quot;0&quot;
              y={8 * index + 2}
            /&amp;gt;
          );
        }
      })}
    &amp;lt;/svg&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>国内首个AIGC黑客马拉松</title><link>https://obiscr.com/blog/aigc-hackathon-2023</link><guid isPermaLink="true">https://obiscr.com/blog/aigc-hackathon-2023</guid><description>本月将于15日和子木一起去参加思否举办的黑客马拉松。</description><content:encoded>&lt;p&gt;import { CardGrid, Card, LinkCard } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;赛事介绍&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;PS：以下内容来来自于思否官方&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;阔别线下赛 4 年之久的 &lt;strong&gt;SegmentFault Hackathon&lt;/strong&gt; 终于在 2023 年重磅回归！&lt;/p&gt;
&lt;p&gt;作为国内首个以 AIGC 为主题的黑客马拉松，我们鼓励开发者大开脑洞，结合日常生活中的痛点需求、兴趣爱好和专业方向，进行 Generative AI 应用的构建，例如：艺术品、音乐、漫画头像、求职简历生成器，智能客服机器人，AI Code Review 工具，基于 AI 的数据可视化平台，医疗图像分析应用等等 —— 以上这些场景联想由 ChatGPT 生成。当然，你也可以选择任何你感兴趣的方向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本届大赛共设有 2 条赛道：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;北京的开发者可以选择线下参加 4 月 15 日- 4 月 16 日 (周六、周日)为期两天 32 小时的黑客马拉松。我们鼓励大家尽可能来到线下，与志同道合者并肩作战，与行业先锋面对面交流；&lt;/li&gt;
&lt;li&gt;为了让更多好创意、好想法、好项目被大家看到，我们专门设置了线上赛道，不便来到线下的远程的开发者也可选择线上参赛。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参赛者在规定时间内以小组为单位完成创意脑暴、产品设计、代码实现和 Demo 展示，大赛评审团将根据应用程序的创新性、技术难度、应用场景、用户体验和商业价值等因素进行评分。&lt;/p&gt;
&lt;p&gt;获奖团队不仅会获得大赛的高额奖金，还有机会获得更广泛的关注和投资。此外，大赛还将邀请人工智能领域的技术大牛和社区领袖，为参赛者们提供专业的建议和支持，以促进这一领域的技术和应用的进一步发展。&lt;/p&gt;
&lt;h2&gt;更多详情&lt;/h2&gt;
&lt;p&gt;有关本次赛事的更多详情，可以看这篇文章里面的赛事安排：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzI3MjEwODMwNQ==&amp;amp;mid=2247497351&amp;amp;idx=1&amp;amp;sn=43f52664dee2feac92421a08e6568979&amp;amp;scene=21#wechat_redirect&quot;&gt;SegmentFault Hackathon 回归！国内首个 AIGC 黑客马拉松来了&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;关注思否&lt;/h2&gt;
&lt;p&gt;微信公众号搜索：&lt;/p&gt;
&lt;p&gt;&amp;lt;CardGrid&amp;gt;
&amp;lt;LinkCard
href=&quot;https://segmentfault.com/&quot;
title=&quot;SegmentFault思否&quot;
/&amp;gt;
&amp;lt;LinkCard
href=&quot;https://segmentfault.com/&quot;
title=&quot;思否技术圈&quot;
/&amp;gt;
&amp;lt;/CardGrid&amp;gt;&lt;/p&gt;
&lt;h2&gt;参与&lt;/h2&gt;
&lt;p&gt;点击 &lt;a href=&quot;https://jinshuju.net/f/nNDEnn&quot;&gt;此处&lt;/a&gt; 即可参与。&lt;/p&gt;
</content:encoded></item><item><title>易经的变卦</title><link>https://obiscr.com/blog/biangua</link><guid isPermaLink="true">https://obiscr.com/blog/biangua</guid><description>主要介绍易经的多种变卦规则，以及他们之间的联系。</description><content:encoded>&lt;p&gt;近期重新制作了在线版的64卦变换表，预览网址：&lt;a href=&quot;https://64gua.online&quot;&gt;https://64gua.online&lt;/a&gt;。效果如上图所示。建议先去体验一下。&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;position: relative; width: 100%; padding-bottom: 56.25%;&quot;&amp;gt;
&amp;lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113068923160062&amp;amp;bvid=BV1vDHyeAE2A&amp;amp;cid=25718489095&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;position: absolute; top: 0; left: 0; width: 100%; height: 100%;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;本卦&lt;/h2&gt;
&lt;p&gt;原始的六十四卦中的任意一卦。本文将以雷火丰 ䷶ 卦为例。在下文进行变卦。&lt;/p&gt;
&lt;h2&gt;错卦（覆卦）&lt;/h2&gt;
&lt;p&gt;将本卦的每一爻阴阳颠倒。例如：阳爻变阴爻，阴爻变阳爻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/cuo-fu-gua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;综卦&lt;/h2&gt;
&lt;p&gt;将本卦从下往上读。例如：本卦的第六爻变为综卦的初爻，本卦的第五爻变为综卦的第二爻，以此类推。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/zonggua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;变卦（之卦）&lt;/h2&gt;
&lt;p&gt;当一个或多个爻发生变化时所得到的新卦。变爻由阴变阳或由阳变阴。变卦的情况比较复杂。主要取决于那个爻变化，而且还可能同时有多个爻发生变化。有多种可能。&lt;/p&gt;
&lt;p&gt;假设第三爻（九三）和第五爻（六五️）发生了变化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/bian-zhi-gua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;互卦&lt;/h2&gt;
&lt;p&gt;由本卦第二、三、四爻组成下卦，第三、四、五爻组成上卦而成的新卦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/hugua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;反卦（游卦）&lt;/h2&gt;
&lt;p&gt;将本卦上下卦位置互换。例如：本卦的下卦变为反卦的上卦，本卦的上卦变为反卦的下卦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/fan-you-gua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;对卦&lt;/h2&gt;
&lt;p&gt;本卦在六十四卦中的对应卦，通常相隔三十二卦。具体规则为：将上卦和下卦互换位置，且将每一爻由阳变阴，由阴变阳。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/duigua.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;卦宫&lt;/h2&gt;
&lt;p&gt;将相关的八个卦归为一类，每个主卦带领七个卦形成一个卦宫。卦宫的划分是基于八纯卦（也称为八经卦），即乾、坤、震、巽、坎、离、艮、兑。
每个卦宫都以这八个卦中的一个为主，称为宫主。每个卦宫包含8个卦：宫主卦加上其他7个下卦不同的卦。&lt;/p&gt;
&lt;p&gt;卦的上卦（即上三爻）决定了它属于哪个卦宫。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/guagong.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;归魂卦&lt;/h2&gt;
&lt;p&gt;当变卦回到本卦时，称为归魂。这种情况在某些特定的卦变中出现。&lt;/p&gt;
&lt;p&gt;在传统的易学解卦中，当卦中只有一个爻发生变化时，我们会关注这个变爻。对于乾卦，无论哪一爻由阳变阴，都会得到一个新的卦。但是，在易经的变卦规则中，如果一个卦中有多于一个爻发生变化，我们就要考虑所有变爻。
乾卦的特殊之处在于：当我们考虑任何一个爻变阴时，从另一个角度看，可以理解为其他五个爻保持不变。在易经的哲学观点中，这种情况被解释为：虽然表面上看起来发生了变化（一爻变阴），但本质上（其他五爻不变）仍然保持了乾卦的特性。
因此，尽管形式上看起来变成了另一个卦，但在易经的解释体系中，这种变化被视为最终仍然回归到乾卦的本质。&lt;/p&gt;
&lt;p&gt;这就是所谓的&quot;归魂&quot;现象。它不是物理上的变化，而是一种哲学和解卦的观点。这种解释强调了乾卦（代表纯阳、强健、创造）的基本性质是如此强大，即使在表面变化中，其本质依然保持不变。&lt;/p&gt;
&lt;h2&gt;别卦&lt;/h2&gt;
&lt;p&gt;与本卦有某种特定关系的其他卦，可能包括上述多种变化方式得到的卦。&lt;/p&gt;
</content:encoded></item><item><title>How to build manimgl from source on macOS</title><link>https://obiscr.com/blog/build-manimgl-from-source</link><guid isPermaLink="true">https://obiscr.com/blog/build-manimgl-from-source</guid><description>This article describes how to build manimgl from source code</description><content:encoded>&lt;p&gt;import LoopingVideo from &apos;~/components/looping-video.astro&apos;
import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;Manim is an engine for precise programmatic animations, designed for creating explanatory math videos.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
Manim has two versions, a personal version maintained by &lt;a href=&quot;https://3blue1brown.com&quot;&gt;3blue1brown&lt;/a&gt; and a community version maintained by &lt;a href=&quot;https://www.manim.community/&quot;&gt;ManimCommunity&lt;/a&gt;.
This article will demo a personal version of the demo on macOS maintained by &lt;a href=&quot;https://3blue1brown.com&quot;&gt;3blue1brown&lt;/a&gt;.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Download source code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd ~/projects
git clone https://github.com/3b1b/manim.git
cd manim
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating an environment&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;conda create -n manim-build-from-source python=3.10
conda activate manim-build-from-source
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Install libraries&lt;/h2&gt;
&lt;h3&gt;mapbox-earcut&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install mapbox-earcut
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ffmpeg&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install ffmpeg
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mactex&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install mactex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tips&quot;&amp;gt;
This may take a little longer.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h4&gt;Verify mactex installation&lt;/h4&gt;
&lt;p&gt;Type &lt;code&gt;latex --version&lt;/code&gt; in the terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;latex --version

# Output
pdfTeX 3.141592653-2.6-1.40.26 (TeX Live 2024)
kpathsea version 6.4.0
Copyright 2024 Han The Thanh (pdfTeX) et al.
There is NO warranty.  Redistribution of this software is
covered by the terms of both the pdfTeX copyright and
the Lesser GNU General Public License.
For more information about these matters, see the file
named COPYING and the pdfTeX source.
Primary author of pdfTeX: Han The Thanh (pdfTeX) et al.
Compiled with libpng 1.6.43; using libpng 1.6.43
Compiled with zlib 1.3.1; using zlib 1.3.1
Compiled with xpdf version 4.04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the output is as above, then congratulations, mactex has been installed successfully. You can proceed to &lt;a href=&quot;#build-from-source&quot;&gt;next step&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Otherwise, please continue. We need to fix this.
I&apos;m not sure why, but sometimes, after brew install mactex finishes, it just downloads a package to your disk. You still need to install it manually.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew info mactex

# Output
❯ brew info mactex
==&amp;gt; mactex: 2024.0312
https://www.tug.org/mactex/
Installed
/opt/homebrew/Caskroom/mactex/2024.0312 (5.6GB)
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/m/mactex.rb
==&amp;gt; Name
MacTeX
==&amp;gt; Description
Full TeX Live distribution with GUI applications
==&amp;gt; Dependencies
ghostscript
==&amp;gt; Artifacts
mactex-20240312.pkg (Pkg)
==&amp;gt; Caveats
You must restart your terminal window for the installation of MacTeX CLI
tools to take effect.

Alternatively, Bash and Zsh users can run the command:

  eval &quot;$(/usr/libexec/path_helper)&quot;

==&amp;gt; Analytics
install: 3,166 (30 days), 7,794 (90 days), 30,329 (365 days)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the highlighted path and open it in the terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;open /opt/homebrew/Caskroom/mactex/2024.031
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see a file named &lt;code&gt;mactex-&amp;lt;date&amp;gt;.pkg&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tree /opt/homebrew/Caskroom/mactex/2024.0312
/opt/homebrew/Caskroom/mactex/2024.0312
└── mactex-20240312.pkg

1 directory, 1 file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open this file and install it manually.&lt;/p&gt;
&lt;p&gt;After the installation is complete, retry &lt;a href=&quot;#verify-mactex-installation&quot;&gt;Verify mactex installation&lt;/a&gt;, now &lt;code&gt;mactex&lt;/code&gt; should work.&lt;/p&gt;
&lt;h2&gt;Build from source&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pip install -e ./manim
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Verify manimgl installation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;which manimgl # output: /opt/anaconda3/envs/manim-build-from-source/bin/manimgl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tips&quot;&amp;gt;
This may vary slightly from system to system, but can be verified by the name of the virtual environment,
as long as &lt;code&gt;manimgl&lt;/code&gt; is from the &lt;code&gt;manim-build-from-source/bin/manimgl&lt;/code&gt; directory. Then there is no problem.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Use manimgl&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd ~/projects/manim
manimgl example_scenes.py TexTransformExample
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should be able to see the animation below.&lt;/p&gt;
&lt;p&gt;&amp;lt;LoopingVideo sources={[{ src: &apos;/public/blog/videos/TexTransformExample.mp4&apos;, type: &apos;video/mp4&apos; }]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;Congratulations, you have completed all the steps.&lt;/p&gt;
&lt;h2&gt;Additional&lt;/h2&gt;
&lt;p&gt;If you otherwise encounter problems with installation and use, you can create an Issue at &lt;a href=&quot;https://github.com/3b1b/manim/issues/new&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Look through the &lt;a href=&quot;https://3b1b.github.io/manim/getting_started/example_scenes.html&quot;&gt;example scenes&lt;/a&gt; to see examples of the library&apos;s syntax, animation types and object types.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://github.com/3b1b/videos&quot;&gt;3b1b/videos&lt;/a&gt; repo, you can see all the code for 3blue1brown videos.&lt;/p&gt;
</content:encoded></item><item><title>Build Your Own Remote Dev Environment Based on VSCode</title><link>https://obiscr.com/blog/building-your-own-remote-dev-env-based-on-vscode</link><guid isPermaLink="true">https://obiscr.com/blog/building-your-own-remote-dev-env-based-on-vscode</guid><description>This article introduces how to build your own remote dev environment based on VSCode.</description><content:encoded>&lt;p&gt;import { Aside, Badge, FileTree, LinkCard } from &apos;@astrojs/starlight/components&apos;;
import ReadMore from &apos;~/components/read-more.astro&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/microsoft/vscode.git&quot;
title=&quot;VSCode Source Code Repository&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;System: &lt;a href=&quot;https://ubuntu.com/download/desktop&quot;&gt;Ubuntu 24.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/zh-cn/download&quot;&gt;Node.js&lt;/a&gt;: v22.21.1&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fly.io/docs/flyctl/install/&quot;&gt;Fly.io CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;Aside&amp;gt;
The platform/account required above please register yourself.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Build VSCode&lt;/h2&gt;
&lt;h3&gt;Download VSCode Source Code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/microsoft/vscode.git
cd vscode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we will build based on a historical version &amp;lt;Badge text=&apos;v1.99.3&apos;/&amp;gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b my-vscode-1.99.3 1.99.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install Dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;ReadMore&amp;gt;See &lt;a href=&quot;https://github.com/microsoft/vscode/wiki/How-to-Contribute&quot;&gt;How to build and run from source&lt;/a&gt;&amp;lt;/ReadMore&amp;gt;&lt;/p&gt;
&lt;h3&gt;Compile VSCode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cd vscode
npm install
npm run compile
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build Docker Image&lt;/h2&gt;
&lt;h3&gt;Create Build Script&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; file in the project root directory, and create &lt;code&gt;build.sh&lt;/code&gt; and &lt;code&gt;entrypoint.sh&lt;/code&gt; files in the scripts directory.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;vscode
&lt;ul&gt;
&lt;li&gt;scripts/
&lt;ul&gt;
&lt;li&gt;build.sh&lt;/li&gt;
&lt;li&gt;entrypoint.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dockerfile
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR=$(cd &quot;$(dirname &quot;$0&quot;)/..&quot; &amp;amp;&amp;amp; pwd)
cd &quot;$ROOT_DIR&quot;

IMAGE_NAME=&quot;${IMAGE_NAME:-vscode-web}&quot;
IMAGE_TAG=&quot;${IMAGE_TAG:-local}&quot;
DOCKERFILE=&quot;${DOCKERFILE:-Dockerfile}&quot;
WEB_DIST_DIR=&quot;${WEB_DIST_DIR:-vscode-reh-web-linux-x64}&quot;

echo &quot;[ENV] IMAGE_NAME=${IMAGE_NAME} IMAGE_TAG=${IMAGE_TAG} DOCKERFILE=${DOCKERFILE}&quot;
echo &quot;[ENV] WEB_DIST_DIR=${WEB_DIST_DIR}&quot;

echo &quot;[STEP] Build VS Code web dist -&amp;gt; ${WEB_DIST_DIR}&quot;
rm -rf &quot;${WEB_DIST_DIR}&quot;
npm run gulp &quot;${WEB_DIST_DIR}&quot;
cp -r ../&quot;${WEB_DIST_DIR}&quot; ./

echo &quot;[STEP] Docker build ${IMAGE_NAME}:${IMAGE_TAG}&quot;
docker build \
  --build-arg WEB_DIST_DIR=&quot;${WEB_DIST_DIR}&quot; \
  -t &quot;${IMAGE_NAME}:${IMAGE_TAG}&quot; \
  -f &quot;${DOCKERFILE}&quot; \
  &quot;${ROOT_DIR}&quot;

echo &quot;[DONE] Built image: ${IMAGE_NAME}:${IMAGE_TAG}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

log() {
  echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] $*&quot;
}

APP_HOME=&quot;${APP_HOME:-/opt/vscode-web}&quot;
HOST=&quot;0.0.0.0&quot;
LISTEN_PORT=&quot;${IDE_PORT:-8080}&quot;
CONNECTION_TOKEN=&quot;${CONNECTION_TOKEN:-rviFq8oBBOIp92fGXnSlWygbjNpGU2FelEeMVQp6CTRiuux0BxGKU01yCCmICBbY}&quot;

ARGS=(
  --host &quot;${HOST}&quot;
  --port &quot;${LISTEN_PORT}&quot;
  --connection-token &quot;${CONNECTION_TOKEN}&quot;
)

log &quot;[INFO] Starting VS Code Web on ${HOST}:${LISTEN_PORT}?tkn=${CONNECTION_TOKEN}&quot;
log &quot;[INFO] APP_HOME=${APP_HOME}&quot;

SERVER_BIN=&quot;${SERVER_BIN:-${APP_HOME}/bin/code-server-oss}&quot;

exec &quot;${SERVER_BIN}&quot; &quot;${ARGS[@]}&quot; &quot;$@&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;FROM ubuntu:24.04

ARG WEB_DIST_DIR=vscode-reh-web-linux-x64

ENV DEBIAN_FRONTEND=noninteractive \
    APP_HOME=/opt/vscode-web \
    IDE_PORT=8080 \
    PNPM_STORE_DIR=/home/vscode/workspace/.pnpm-store

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    bash \
    dumb-init \
    git \
    unzip \
    zip \
    wget \
    net-tools \
    lrzsz \
    gnupg \
 &amp;amp;&amp;amp; curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
 &amp;amp;&amp;amp; apt-get install -y --no-install-recommends nodejs \
 &amp;amp;&amp;amp; npm install -g pnpm@9 \
 &amp;amp;&amp;amp; npm cache clean --force \
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

RUN groupadd -r vscode &amp;amp;&amp;amp; useradd -m -r -g vscode -s /bin/bash vscode

RUN mkdir -p &quot;$APP_HOME&quot; /home/vscode/workspace &quot;$PNPM_STORE_DIR&quot; \
 &amp;amp;&amp;amp; chown -R vscode:vscode &quot;$APP_HOME&quot; /home/vscode

COPY ${WEB_DIST_DIR}/ &quot;$APP_HOME&quot;/

COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod 0755 /usr/local/bin/entrypoint.sh \
 &amp;amp;&amp;amp; chown -R vscode:vscode &quot;$APP_HOME&quot;

USER vscode

RUN pnpm config set store-dir &quot;$PNPM_STORE_DIR&quot; --global \
 &amp;amp;&amp;amp; git config --global user.name &quot;vscode&quot; \
 &amp;amp;&amp;amp; git config --global user.email &quot;vscode@local.com&quot;

WORKDIR /home/vscode/workspace

EXPOSE 8080

ENTRYPOINT [&quot;/usr/bin/dumb-init&quot;, &quot;--&quot;]
CMD [&quot;/usr/local/bin/entrypoint.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start building the image.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd vscode
bash scripts/build.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the build is complete, you can use &lt;code&gt;docker images&lt;/code&gt; to view the built image.&lt;/p&gt;
&lt;h3&gt;Push Image to Dockerhub&lt;/h3&gt;
&lt;p&gt;Register a &lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt; account, and install &lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside&amp;gt;
Replace &lt;code&gt;{username}&lt;/code&gt; with your own username.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker login
docker tag vscode-web:local {username}/vscode-web:latest
docker push {username}/vscode-web:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy to Fly.io&lt;/h2&gt;
&lt;p&gt;&amp;lt;Aside&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;{username}&lt;/code&gt; with your own username.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;region&lt;/code&gt; in the &lt;code&gt;fly.toml&lt;/code&gt; below. Try to choose the closest one to you. All available &lt;code&gt;regions&lt;/code&gt; are available at &lt;a href=&quot;https://fly.io/docs/reference/regions/&quot;&gt;Fly.io Regions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;app&lt;/code&gt; in the &lt;code&gt;fly.toml&lt;/code&gt; below must be unique, if it is repeated, it will fail to create. You can add some random characters. As long as it is not repeated.
&amp;lt;/Aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Login Fly.io&lt;/h3&gt;
&lt;p&gt;Register a &lt;a href=&quot;https://fly.io/&quot;&gt;Fly.io&lt;/a&gt; account, and install &lt;a href=&quot;https://fly.io/docs/flyctl/install/&quot;&gt;Fly.io CLI&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fly auth login
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configure Token&lt;/h3&gt;
&lt;p&gt;Configure the &lt;code&gt;access_token&lt;/code&gt; in the &lt;code&gt;~/.fly/config.yml&lt;/code&gt; file to the &lt;code&gt;FLY_API_TOKEN&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export FLY_API_TOKEN=$(cat ~/.fly/config.yml | grep access_token | awk -F &apos;: &apos; &apos;{print $2}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create App&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fly apps create {username}-vscode-web-ide
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create Volume&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fly volumes create vscode_workspace --size 3 --region sin --app {username}-vscode-web-ide
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create fly.toml file&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;fly.toml&lt;/code&gt; file in the project root directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app = &apos;{username}-vscode-web-ide&apos;
primary_region = &apos;sjc&apos;

[build]
  image = &apos;{username}/vscode-web:latest&apos;

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = &apos;suspend&apos;
  auto_start_machines = true
  min_machines_running = 0
  processes = [&apos;app&apos;]

[[vm]]
  memory = &apos;4gb&apos;
  cpu_kind = &apos;shared&apos;
  cpus = 8

[[mounts]]
  source = &quot;vscode_workspace&quot;
  destination = &quot;/home/vscode/workspace&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you can start deploying.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fly deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Access VSCode Web&lt;/h2&gt;
&lt;p&gt;If everything is normal, then you can now access VSCode Web normally.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://{username}-vscode-web-ide.fly.dev?tkn=rviFq8oBBOIp92fGXnSlWygbjNpGU2FelEeMVQp6CTRiuux0BxGKU01yCCmICBbY
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>ChatGPT插件全球首发</title><link>https://obiscr.com/blog/chatgpt-first-release</link><guid isPermaLink="true">https://obiscr.com/blog/chatgpt-first-release</guid><description>ChatGPT JetBrains 插件全球首发，你离大佬只差一个AI。</description><content:encoded>&lt;p&gt;import { CardGrid, Card, LinkButton, LinkCard } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;最近ChatGPT在技术圈子很热，忽然灵感迸发。想着能不能放到IDE里面。于是这款插件诞生了。&lt;/p&gt;
&lt;h2&gt;预览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/learn-binary-tree.png&quot; alt=&quot;Learn binary tree&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/explain-codes.png&quot; alt=&quot;Explain codes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/code-insert-to-editor.png&quot; alt=&quot;Insert code to editor&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/code-generation.png&quot; alt=&quot;Code generation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上面的演示，可以直接在 IDE 进行操作。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;&amp;lt;LinkButton href=&quot;https://plugins.jetbrains.com/plugin/20603-chatgpt&quot;&amp;gt;查看插件市场&amp;lt;/LinkButton&amp;gt;&lt;/p&gt;
&lt;p&gt;打开 Settings/Preferences - Plugins - Marketplace - 搜索 &quot;ChatGPT&quot; - 安装插件&lt;/p&gt;
&lt;h2&gt;兼容的版本&lt;/h2&gt;
&lt;p&gt;2020.1 ~ 2024.*&lt;/p&gt;
&lt;h2&gt;相关链接&lt;/h2&gt;
&lt;p&gt;&amp;lt;CardGrid&amp;gt;
&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/ChatGPT.git&quot;
title=&quot;Github&quot;
/&amp;gt;
&amp;lt;LinkCard
href=&quot;https://plugins.jetbrains.com/plugin/20603-chatgpt&quot;
title=&quot;插件市场&quot;
/&amp;gt;
&amp;lt;LinkCard
href=&quot;https://chatgpt.gold&quot;
title=&quot;文档&quot;
/&amp;gt;
&amp;lt;/CardGrid&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>ExcelReader reaches 1 million downloads</title><link>https://obiscr.com/blog/excelreader-1-million-downloads</link><guid isPermaLink="true">https://obiscr.com/blog/excelreader-1-million-downloads</guid><description>ExcelReader reaches 1 million downloads, I really appreciate everyone who downloads and uses ExcelReader!</description><content:encoded>&lt;p&gt;import { LinkCard, Steps, Card } from &apos;@astrojs/starlight/components&apos;;
import ReadMore from &apos;~/components/read-more.astro&apos;;&lt;/p&gt;
&lt;h2&gt;What&apos;s ExcelReader&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/14722-excelreader&quot;&gt;&lt;img src=&quot;https://img.shields.io/jetbrains/plugin/d/14722-excelreader.svg&quot; alt=&quot;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;ReadMore&amp;gt;ExcelReader is a plugin for the JetBrains platform. It can parse files in three formats of xls, xlsx, csv in most editors of JetBrains, and supports more powerful data filtering functions.&amp;lt;/ReadMore&amp;gt;&lt;/p&gt;
&lt;h2&gt;Why this project&lt;/h2&gt;
&lt;p&gt;Everything some that originated on July 27, 2020 one afternoon. My job at that time required working with very large amounts of Excel data in the IDE. I was mostly using Office at that time. however, I wasn&apos;t going to do anything complex with Excel, just want to preview the contents of the file, but Office was too heavyweight. Window switching frustrated me. So the idea of writing an add-in was born. So this project was born.&lt;/p&gt;
&lt;h2&gt;Some interesting history&lt;/h2&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Initial Design Challenges&lt;/p&gt;
&lt;p&gt;I remember at the very beginning, what I wanted was to put the data view into the Editor area instead of the ToolWindow area, just like the DataGrip. But I couldn&apos;t do that due to the limitations of the technology at the time, so I ended up putting it in the ToolWindow area.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Design Philosophy&lt;/p&gt;
&lt;p&gt;However, with my current vision, I insist that putting it in the ToolWindow area is a better option. Because, this plugin is not a professional data processing tool. So it shouldn&apos;t occupy the main Editor area, it&apos;s just an auxiliary tool to provide more help when necessary.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;User Feedback &amp;amp; Evolution&lt;/p&gt;
&lt;p&gt;During the first year or two of ExcelReader&apos;s release, I would receive feedback from users who thought it would be nice to support data writing. So finally, in February 2022, I started the development of the ExcelEditor project, an advanced version of ExcelReader.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Development Challenges&lt;/p&gt;
&lt;p&gt;But unfortunately, I don&apos;t have much time to spend on these projects. I spent a lot of time in the early stages of the project because I needed to learn how to implement these features. I will have a fixed time each week to develop the ExcelEditor project.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pricing Strategy&lt;/p&gt;
&lt;p&gt;ExcelEditor retails for $3.90, which I think is moderately priced; it&apos;s not cheap, but it&apos;s not expensive either. I think it depends more on how well it can help you.&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://plugins.jetbrains.com/plugin/18663-exceleditor&quot;
title=&quot;ExcelEditor&quot;
description=&quot;ExcelEditor is a plugin for the JetBrains platform. It can parse files in three formats of xls, xlsx, csv in most editors of JetBrains, and supports more powerful data filtering functions.&quot;
/&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Development Challenges&lt;/p&gt;
&lt;p&gt;Let&apos;s go back to ExcelReader. honestly, I didn&apos;t even think that this plugin would become so popular, I remember a time when I didn&apos;t pay attention to this plugin at all, but by the time I did, he already had more than 500K downloads. Until the year 2025... , I forget what month it was. Anyway, it crossed 1 million downloads.
&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;What exciting news. I think a million downloads is more than just a number, it&apos;s the trust and choice of countless developers, and I&apos;m really grateful to everyone who has downloaded and used ExcelReader, the contributors who have provided valuable feedback to help the plugin grow. And my family, who have helped me a lot as well. Anyway, thank you all.&lt;/p&gt;
</content:encoded></item><item><title>解决 intellij 插件构建时 gradle 下载缓慢的问题</title><link>https://obiscr.com/blog/fix-slow-gradle-downloads-for-intellij-plugin-builds</link><guid isPermaLink="true">https://obiscr.com/blog/fix-slow-gradle-downloads-for-intellij-plugin-builds</guid><description>在部分国家和地区，构建intellij插件的时候，gradle需要下载一些SDK相关的文件，但是某些情况下会非常慢，这里提供一种可行的解决方法。</description><content:encoded>&lt;p&gt;import { LinkCard } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;下载 IDE&lt;/h2&gt;
&lt;p&gt;在下载之前，需要知道IDE的的类型和版本，例如下面的片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

# IntelliJ Platform Properties -&amp;gt; https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IC
platformVersion = 2022.2.5

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它一般会配置在 &lt;a href=&quot;https://github.com/JetBrains/intellij-platform-plugin-template/blob/main/gradle.properties#L15&quot;&gt;gradle.properties&lt;/a&gt; 文件.
当然这个也取决于自己的项目配置。但我相信这应该不会难到你。我们可以很轻松就得到这个上面两个信息。&lt;/p&gt;
&lt;p&gt;我们一共需要下载两个文件，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IDE发行版（ideaIC-2022.2.5）&lt;/li&gt;
&lt;li&gt;IDE发行版源码包（ideaIC-2022.2.5-sources.jar）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载发行版&lt;/h3&gt;
&lt;p&gt;URL的格式为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://cache-redirector.jetbrains.com/www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/idea&amp;lt;platformType&amp;gt;/&amp;lt;platformVersion&amp;gt;/idea&amp;lt;platformType&amp;gt;-&amp;lt;platformVersion&amp;gt;.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带入类型和版本之后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://cache-redirector.jetbrains.com/www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2022.2.5/ideaIC-2022.2.5.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件下载完成以后，需要放到 gradle 目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/idea&amp;lt;platformType&amp;gt;/&amp;lt;platformVersion&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带入类型和版本之后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有的话，需要先新建这个目录&lt;/p&gt;
&lt;p&gt;然后计算下载的这个文件的SHA1值&lt;/p&gt;
&lt;p&gt;在Windows可以使用计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certutil -hashfile &amp;lt;path\to\ideaIC-2022.2.5.zip&amp;gt; SHA1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在macOS / Linux 可以使用计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shasum -a 1 &amp;lt;/path/to/ideaIC-2022.2.5.zip&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再刚才的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面在新建一个文件夹，文件夹的名字就是
计算出来的SHA1的值。&lt;/p&gt;
&lt;p&gt;例如我们计算出来的SHA1是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;836d1896d66b8d410f65d76e8cbc910b16d9edab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建文件夹之后，进入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5/836d1896d66b8d410f65d76e8cbc910b16d9edab**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件夹。然后把 &lt;code&gt;ideaIC-2022.2.5.zip&lt;/code&gt; 放到这个文件夹里面。&lt;/p&gt;
&lt;h3&gt;下载发行版源码包&lt;/h3&gt;
&lt;p&gt;URL的格式为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://cache-redirector.jetbrains.com/www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/idea&amp;lt;platformType&amp;gt;/&amp;lt;platformVersion&amp;gt;/idea&amp;lt;platformType&amp;gt;-&amp;lt;platformVersion&amp;gt;-sources.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带入类型和版本之后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://cache-redirector.jetbrains.com/www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2022.2.5/ideaIC-2022.2.5-sources.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件下载完成以后，需要放到 gradle 目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/idea&amp;lt;platformType&amp;gt;/&amp;lt;platformVersion&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带入类型和版本之后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有的话，需要先新建这个目录&lt;/p&gt;
&lt;p&gt;然后计算下载的这个文件的SHA1值&lt;/p&gt;
&lt;p&gt;在Windows可以使用计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certutil -hashfile &amp;lt;path\to\ideaIC-2022.2.5-sources.jar&amp;gt; SHA1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在macOS / Linux 可以使用计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shasum -a 1 &amp;lt;/path/to/ideaIC-2022.2.5-sources.jar&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再刚才的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面在新建一个文件夹，文件夹的名字就是
计算出来的SHA1的值。&lt;/p&gt;
&lt;p&gt;例如我们计算出来的SHA1是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d96d1896d66b8d410f65d76e8cbc910b16d9edac**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建文件夹之后，进入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.gradle/caches/modules-2/files-2.1/com.jetbrains.intellij.idea/ideaIC/2022.2.5/d96d1896d66b8d410f65d76e8cbc910b16d9edac**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件夹。然后把 &lt;code&gt;ideaIC-2022.2.5-sources.jar&lt;/code&gt; 放到这个文件夹里面。&lt;/p&gt;
&lt;h2&gt;相关工具&lt;/h2&gt;
&lt;p&gt;你可以在这里在下方下载相关的工具。&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/intellij-sdk-helper&quot;
title=&quot;intellij-sdk-helper&quot;
description=&quot;这是一个用于 IntelliJ IDEA 插件开发的辅助工具&quot;
/&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>How to give developers feedback</title><link>https://obiscr.com/blog/how-to-report-bugs</link><guid isPermaLink="true">https://obiscr.com/blog/how-to-report-bugs</guid><description>When a plugin encounters a problem, perhaps some of the following are ways to solve the problem faster and better.</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;How to give developers feedback&lt;/h2&gt;
&lt;p&gt;Inevitably, we will encounter some problems when using plugins. To solve problems quickly, we need a way to communicate with developers quickly and efficiently. Reduce the possible time loss and useless repetitive operations, and solve the problem as quickly as possible.&lt;/p&gt;
&lt;h2&gt;An error occurs&lt;/h2&gt;
&lt;p&gt;Here, for example, we use the &lt;a href=&quot;https://plugins.jetbrains.com/plugin/20603-chatgpt--easycode&quot;&gt;ChatGPT - EasyCode&lt;/a&gt; plugin to simulate an error.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/bug-report.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The markers here are&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plgugin version info&lt;/li&gt;
&lt;li&gt;Author&apos;s e-mail or website&lt;/li&gt;
&lt;li&gt;Error message&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Try contacting the author&lt;/h2&gt;
&lt;p&gt;As you can see at &lt;code&gt;marker 2&lt;/code&gt;, you can find the plugin author&apos;s customized &lt;strong&gt;website or email&lt;/strong&gt;. First of all, where one can try to ask for help. It is also possible to ask for help in the forums (if there are any), maybe you are not the only one experiencing this problem. It is also possible to leave a message on the plugin&apos;s Review&apos;s page.&lt;/p&gt;
&lt;p&gt;Anyway, the first thing to do is to make sure that there is at least one way to contact the author.&lt;/p&gt;
&lt;h2&gt;Providing necessary information&lt;/h2&gt;
&lt;p&gt;When an error occurs, the most important information for developers to know is the following：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Error log&lt;/strong&gt; (At &lt;code&gt;marker 3&lt;/code&gt; in the image above): The information here is important. In most cases, we can quickly locate the problem based on this information alone. Copy everything here and put it in an attachment to the author. This is very necessary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugin version&lt;/strong&gt; (At &lt;code&gt;marker 1&lt;/code&gt; in the image above): When a plugin error occurs, we can usually see the plugin version, which is also an important piece of information.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE version&lt;/strong&gt;: There are cases where SDK compatibility or other reasons cause certain bugs to only appear on a specific version of the IDE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idea.log&lt;/strong&gt;: This is a more complete log file for ide and will record some information about the ide/plugin. In some cases, the plugin author may also need you to provide this file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to find idea.log&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/find-idea-log.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Help&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Show Log in Explorer&lt;/strong&gt; in the menu bar.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/idea-location.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Select to see a file with the name &lt;code&gt;idea.log&lt;/code&gt;. That&apos;s the file we&apos;re looking for.&lt;/p&gt;
&lt;p&gt;Here my file path is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\obiscr\AppData\Local\JetBrains\IntelliJIdea2023.2\log\idea.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
This is just an example of a Windows system. For more detailed information about log locations, you can refer to &lt;a href=&quot;https://intellij-support.jetbrains.com/hc/en-us/articles/206544519-Directories-used-by-the-IDE-to-store-settings-caches-plugins-and-logs&quot;&gt;Directories used by the IDE to store settings, caches, plugins and logs&lt;/a&gt;
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;More complex situations&lt;/h2&gt;
&lt;p&gt;Usually, with the above, the problem can be solved. But sometimes, there are some weird cases, even the author can&apos;t solve it, or maybe it&apos;s still a bug in the IDE itself. so the bug fixing cycle is not very fixed.&lt;/p&gt;
&lt;h2&gt;Summarize&lt;/h2&gt;
&lt;p&gt;When in question, we can ask the author for as much information as possible in total at once. For example, all of the above mentioned.
If there is only part of it, the author may ask you for other necessary information. This communication is usually more time consuming.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
This is true for any problem, in fact, we can provide as much information as possible, always save some time spent on communication and also allow the problem to be solved as soon as possible.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>How to speed up pnpm postinstall</title><link>https://obiscr.com/blog/how-to-speed-up-pnpm-postinstall</link><guid isPermaLink="true">https://obiscr.com/blog/how-to-speed-up-pnpm-postinstall</guid><description>This article will introduce how to speed up pnpm postinstall through the example of supabase cli.</description><content:encoded>&lt;p&gt;import { Aside, Badge, FileTree } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;Problem description&lt;/h2&gt;
&lt;p&gt;If you have used supabase cli in your project, you may have encountered the following problem:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; contains the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running &lt;code&gt;pnpm install&lt;/code&gt;, you will see the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) obiscr@192 my-project % pnpm install

   ╭──────────────────────────────────────────╮
   │                                          │
   │   Update available! 10.10.0 → 10.14.0.   │
   │   Changelog: https://pnpm.io/v/10.14.0   │
   │     To update, run: pnpm self-update     │
   │                                          │
   ╰──────────────────────────────────────────╯

 WARN  deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
 WARN  6 deprecated subdependencies found: @humanwhocodes/config-array@0.13.0, @humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0, rimraf@3.0.2
Packages: +417
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 WARN  Failed to create bin at /Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/bin/supabase&apos;
Progress: resolved 433, reused 382, downloaded 20, added 417, done
 WARN  Failed to create bin at /Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/bin/supabase&apos;
node_modules/supabase: Running postinstall script, failed in 3m 52.2s
node_modules/supabase postinstall$ node scripts/postinstall.js
│ Downloading https://github.com/supabase/cli/releases/download/v2.34.3/supabase_2.34.3_checksums.txt
│ Downloading https://github.com/supabase/cli/releases/download/v2.34.3/supabase_darwin_arm64.tar.gz
│ Warning: Detected unsettled top-level await at file:///Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/su…
│ await main();
│ ^
└─ Failed in 3m 52.2s at /Users/obiscr/projects/my-project/node_modules/supabase
node_modules/unrs-resolver: Running postinstall script, done in 199ms
 ELIFECYCLE  Command failed with exit code 13.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It took nearly 4 minutes, but the installation still failed. After multiple tests, this situation often occurs when the network conditions are poor.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot;&amp;gt;
This article uses &amp;lt;Badge text=&quot;supabase cli&quot; variant=&quot;note&quot; /&amp;gt; as an example, but this method is universal. The essence is to change the &lt;code&gt;postinstall.js&lt;/code&gt; file, so that it uses the local file first when installing, and if the local file does not exist, it executes the default logic to download from &lt;code&gt;github&lt;/code&gt;.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;p&gt;Since the &lt;code&gt;postinstall&lt;/code&gt; script of &lt;code&gt;supabase cli&lt;/code&gt; failed to install, let&apos;s take a look at what the &lt;code&gt;postinstall&lt;/code&gt; script of &lt;code&gt;supabase cli&lt;/code&gt; does.&lt;/p&gt;
&lt;p&gt;Enter the &lt;code&gt;node_modules/supabase&lt;/code&gt; directory, the structure is as follows:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;supabase
&lt;ul&gt;
&lt;li&gt;bin/&lt;/li&gt;
&lt;li&gt;scripts/
&lt;ul&gt;
&lt;li&gt;postinstall.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LICENSE&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;li&gt;README.md
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;postinstall.js&lt;/code&gt; contains the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

// Ref 1: https://github.com/sanathkr/go-npm
// Ref 2: https://medium.com/xendit-engineering/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
&quot;use strict&quot;;

import binLinks from &quot;bin-links&quot;;
import { createHash } from &quot;crypto&quot;;
import fs from &quot;fs&quot;;
import fetch from &quot;node-fetch&quot;;
import { Agent } from &quot;https&quot;;
import { HttpsProxyAgent } from &quot;https-proxy-agent&quot;;
import path from &quot;path&quot;;
import { extract } from &quot;tar&quot;;
import zlib from &quot;zlib&quot;;

// Mapping from Node&apos;s `process.arch` to Golang&apos;s `$GOARCH`
const ARCH_MAPPING = {
  x64: &quot;amd64&quot;,
  arm64: &quot;arm64&quot;,
};

// Mapping between Node&apos;s `process.platform` to Golang&apos;s
const PLATFORM_MAPPING = {
  darwin: &quot;darwin&quot;,
  linux: &quot;linux&quot;,
  win32: &quot;windows&quot;,
};

const arch = ARCH_MAPPING[process.arch];
const platform = PLATFORM_MAPPING[process.platform];

// TODO: import pkg from &quot;../package.json&quot; assert { type: &quot;json&quot; };
const readPackageJson = async () =&amp;gt; {
  const contents = await fs.promises.readFile(&quot;package.json&quot;);
  return JSON.parse(contents);
};

// Build the download url from package.json
const getDownloadUrl = (packageJson) =&amp;gt; {
  const pkgName = packageJson.name;
  const version = packageJson.version;
  const repo = packageJson.repository;
  const url = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${platform}_${arch}.tar.gz`;
  return url;
};

const fetchAndParseCheckSumFile = async (packageJson, agent) =&amp;gt; {
  const version = packageJson.version;
  const pkgName = packageJson.name;
  const repo = packageJson.repository;
  const checksumFileUrl = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${version}_checksums.txt`;

  // Fetch the checksum file
  console.info(&quot;Downloading&quot;, checksumFileUrl);
  const response = await fetch(checksumFileUrl, { agent });
  if (response.ok) {
    const checkSumContent = await response.text();
    const lines = checkSumContent.split(&quot;\n&quot;);

    const checksums = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      checksums[packageName] = checksum;
    }

    return checksums;
  } else {
    console.error(
      &quot;Could not fetch checksum file&quot;,
      response.status,
      response.statusText
    );
  }
};

const errGlobal = `Installing Supabase CLI as a global module is not supported.
Please use one of the supported package managers: https://github.com/supabase/cli#install-the-cli
`;
const errChecksum = &quot;Checksum mismatch. Downloaded data might be corrupted.&quot;;
const errUnsupported = `Installation is not supported for ${process.platform} ${process.arch}`;

/**
 * Reads the configuration from application&apos;s package.json,
 * downloads the binary from package url and stores at
 * ./bin in the package&apos;s root.
 *
 *  See: https://docs.npmjs.com/files/package.json#bin
 */
async function main() {
  const yarnGlobal = JSON.parse(
    process.env.npm_config_argv || &quot;{}&quot;
  ).original?.includes(&quot;global&quot;);
  if (process.env.npm_config_global || yarnGlobal) {
    throw errGlobal;
  }
  if (!arch || !platform) {
    throw errUnsupported;
  }

  // Read from package.json and prepare for the installation.
  const pkg = await readPackageJson();
  if (platform === &quot;windows&quot;) {
    // Update bin path in package.json
    pkg.bin[pkg.name] += &quot;.exe&quot;;
  }

  // Prepare the installation path by creating the directory if it doesn&apos;t exist.
  const binPath = pkg.bin[pkg.name];
  const binDir = path.dirname(binPath);
  await fs.promises.mkdir(binDir, { recursive: true });

  // Create the agent that will be used for all the fetch requests later.
  const proxyUrl =
    process.env.npm_config_https_proxy ||
    process.env.npm_config_http_proxy ||
    process.env.npm_config_proxy;
  // Keeps the TCP connection alive when sending multiple requests
  // Ref: https://github.com/node-fetch/node-fetch/issues/1735
  const agent = proxyUrl
    ? new HttpsProxyAgent(proxyUrl, { keepAlive: true })
    : new Agent({ keepAlive: true });

  // First, fetch the checksum map.
  const checksumMap = await fetchAndParseCheckSumFile(pkg, agent);

  // Then, download the binary.
  const url = getDownloadUrl(pkg);
  console.info(&quot;Downloading&quot;, url);
  const resp = await fetch(url, { agent });
  const hash = createHash(&quot;sha256&quot;);
  const pkgNameWithPlatform = `${pkg.name}_${platform}_${arch}.tar.gz`;

  // Then, decompress the binary -- we will first Un-GZip, then we will untar.
  const ungz = zlib.createGunzip();
  const binName = path.basename(binPath);
  const untar = extract({ cwd: binDir }, [binName]);

  // Update the hash with the binary data as it&apos;s being downloaded.
  resp.body
    .on(&quot;data&quot;, (chunk) =&amp;gt; {
      hash.update(chunk);
    })
    // Pipe the data to the ungz stream.
    .pipe(ungz);

  // After the ungz stream has ended, verify the checksum.
  ungz
    .on(&quot;end&quot;, () =&amp;gt; {
      const expectedChecksum = checksumMap?.[pkgNameWithPlatform];
      // Skip verification if we can&apos;t find the file checksum
      if (!expectedChecksum) {
        console.warn(&quot;Skipping checksum verification&quot;);
        return;
      }
      const calculatedChecksum = hash.digest(&quot;hex&quot;);
      if (calculatedChecksum !== expectedChecksum) {
        throw errChecksum;
      }
      console.info(&quot;Checksum verified.&quot;);
    })
    // Pipe the data to the untar stream.
    .pipe(untar);

  // Wait for the untar stream to finish.
  await new Promise((resolve, reject) =&amp;gt; {
    untar.on(&quot;error&quot;, reject);
    untar.on(&quot;end&quot;, () =&amp;gt; resolve());
  });

  // Link the binaries in postinstall to support yarn
  await binLinks({
    path: path.resolve(&quot;.&quot;),
    pkg: { ...pkg, bin: { [pkg.name]: binPath } },
  });

  console.info(&quot;Installed Supabase CLI successfully&quot;);
}

await main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In short, it downloads the binary file of &lt;code&gt;supabase&lt;/code&gt; from the &lt;code&gt;github&lt;/code&gt; repository of &lt;code&gt;supabase&lt;/code&gt; and installs it into &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, can we optimize this process?&lt;/p&gt;
&lt;p&gt;The answer is yes.&lt;/p&gt;
&lt;p&gt;We can download the binary file of &lt;code&gt;supabase&lt;/code&gt; to the local, and then install it into &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Execute the following command in the terminal of the project root directory to download the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L -o lib/supabase/supabase_2.34.3_checksums.txt https://github.com/supabase/cli/releases/download/v2.34.3/supabase_2.34.3_checksums.txt
curl -L -o lib/supabase/supabase_darwin_arm64.tar.gz https://github.com/supabase/cli/releases/download/v2.34.3/supabase_darwin_arm64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execute &lt;code&gt;pnpm patch supabase@2.34.3&lt;/code&gt; in the terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm patch supabase@2.34.3
Patch: You can now edit the package at:

  /Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3

To commit your changes, run:

  pnpm patch-commit &apos;/Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;node_modules/.pnpm_patches/supabase@2.34.3&lt;/code&gt; directory, you will see the following content:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node_modules/
&lt;ul&gt;
&lt;li&gt;.pnpm_patches
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3
&lt;ul&gt;
&lt;li&gt;scripts
&lt;ul&gt;
&lt;li&gt;postinstall.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LICENSE&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;li&gt;README.md&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;state.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then edit the &lt;code&gt;postinstall.js&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

// Ref 1: https://github.com/sanathkr/go-npm
// Ref 2: https://medium.com/xendit-engineering/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
&quot;use strict&quot;;

import binLinks from &quot;bin-links&quot;;
import { createHash } from &quot;crypto&quot;;
import fs from &quot;fs&quot;;
import fetch from &quot;node-fetch&quot;;
import { Agent } from &quot;https&quot;;
import { HttpsProxyAgent } from &quot;https-proxy-agent&quot;;
import path from &quot;path&quot;;
import { extract } from &quot;tar&quot;;
import zlib from &quot;zlib&quot;;

// Mapping from Node&apos;s `process.arch` to Golang&apos;s `$GOARCH`
const ARCH_MAPPING = {
  x64: &quot;amd64&quot;,
  arm64: &quot;arm64&quot;,
};

// Mapping between Node&apos;s `process.platform` to Golang&apos;s
const PLATFORM_MAPPING = {
  darwin: &quot;darwin&quot;,
  linux: &quot;linux&quot;,
  win32: &quot;windows&quot;,
};

const arch = ARCH_MAPPING[process.arch];
const platform = PLATFORM_MAPPING[process.platform];

// TODO: import pkg from &quot;../package.json&quot; assert { type: &quot;json&quot; };
const readPackageJson = async () =&amp;gt; {
  const contents = await fs.promises.readFile(&quot;package.json&quot;);
  return JSON.parse(contents);
};

// Build the download url from package.json
const getDownloadUrl = (packageJson) =&amp;gt; {
  const pkgName = packageJson.name;
  const version = packageJson.version;
  const repo = packageJson.repository;
  const url = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${platform}_${arch}.tar.gz`;
  return url;
};

const fetchAndParseCheckSumFile = async (packageJson, agent) =&amp;gt; {
  const version = packageJson.version;
  const pkgName = packageJson.name;
  const repo = packageJson.repository;
  const checksumFileUrl = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${version}_checksums.txt`;

  // Fetch the checksum file
  console.info(&quot;Downloading&quot;, checksumFileUrl);
  const response = await fetch(checksumFileUrl, { agent });
  if (response.ok) {
    const checkSumContent = await response.text();
    const lines = checkSumContent.split(&quot;\n&quot;);

    const checksums = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      checksums[packageName] = checksum;
    }

    return checksums;
  } else {
    console.error(
      &quot;Could not fetch checksum file&quot;,
      response.status,
      response.statusText
    );
  }
};

const errGlobal = `Installing Supabase CLI as a global module is not supported.
Please use one of the supported package managers: https://github.com/supabase/cli#install-the-cli
`;
const errChecksum = &quot;Checksum mismatch. Downloaded data might be corrupted.&quot;;
const errUnsupported = `Installation is not supported for ${process.platform} ${process.arch}`;

/**
 * Reads the configuration from application&apos;s package.json,
 * downloads the binary from package url and stores at
 * ./bin in the package&apos;s root.
 *
 *  See: https://docs.npmjs.com/files/package.json#bin
 */
async function main() {
  const yarnGlobal = JSON.parse(
    process.env.npm_config_argv || &quot;{}&quot;
  ).original?.includes(&quot;global&quot;);
  if (process.env.npm_config_global || yarnGlobal) {
    throw errGlobal;
  }
  if (!arch || !platform) {
    throw errUnsupported;
  }

  // Read from package.json and prepare for the installation.
  const pkg = await readPackageJson();
  if (platform === &quot;windows&quot;) {
    // Update bin path in package.json
    pkg.bin[pkg.name] += &quot;.exe&quot;;
  }

  // Prepare the installation path by creating the directory if it doesn&apos;t exist.
  const binPath = pkg.bin[pkg.name];
  const binDir = path.dirname(binPath);
  await fs.promises.mkdir(binDir, { recursive: true });

  // Create the agent that will be used for all the fetch requests later.
  const proxyUrl =
    process.env.npm_config_https_proxy ||
    process.env.npm_config_http_proxy ||
    process.env.npm_config_proxy;
  // Keeps the TCP connection alive when sending multiple requests
  // Ref: https://github.com/node-fetch/node-fetch/issues/1735
  const agent = proxyUrl
    ? new HttpsProxyAgent(proxyUrl, { keepAlive: true })
    : new Agent({ keepAlive: true });

  // First, fetch the checksum map.
  const checksumMap = await fetchAndParseCheckSumFile(pkg, agent);
  // Prefer local resources if present (project root via INIT_CWD)
  const projectRoot = process.env.INIT_CWD || process.cwd();
  const resourcesDir = path.join(projectRoot, &quot;lib&quot;, &quot;supabase&quot;);
  const localChecksumPath = path.join(resourcesDir, `${pkg.name}_${pkg.version}_checksums.txt`);
  const localTgzPath = path.join(resourcesDir, `${pkg.name}_${platform}_${arch}.tar.gz`);
  
  // First, read checksum map from local file if available; otherwise fetch
  let checksumMap;
  try {
    const checkSumContent = await fs.promises.readFile(localChecksumPath, &quot;utf8&quot;);
    const lines = checkSumContent.split(&quot;\n&quot;);
    checksumMap = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      if (packageName) checksumMap[packageName] = checksum;
    }
    console.info(&quot;Using local checksum file&quot;, localChecksumPath);
  } catch {
    // fallback to remote checksum
    checksumMap = await fetchAndParseCheckSumFile(pkg, agent);
  }

  // Then, download the binary.
  const url = getDownloadUrl(pkg);
  console.info(&quot;Downloading&quot;, url);
  const resp = await fetch(url, { agent });
  const hash = createHash(&quot;sha256&quot;);
  const pkgNameWithPlatform = `${pkg.name}_${platform}_${arch}.tar.gz`;

  // Then, decompress the binary -- we will first Un-GZip, then we will untar.
  const ungz = zlib.createGunzip();
  const binName = path.basename(binPath);
  const untar = extract({ cwd: binDir }, [binName]);

  // Update the hash with the binary data as it&apos;s being downloaded.
  resp.body
  let sourceStream;
  let usingLocal = false;
  try {
    await fs.promises.access(localTgzPath, fs.constants.R_OK);
    usingLocal = true;
  } catch {}
  if (usingLocal) {
    console.info(&quot;Using local tgz&quot;, localTgzPath);
    sourceStream = fs.createReadStream(localTgzPath);
  } else {
    console.info(&quot;Downloading&quot;, url);
    const resp = await fetch(url, { agent });
    sourceStream = resp.body;
  }
  sourceStream
    .on(&quot;data&quot;, (chunk) =&amp;gt; {
      hash.update(chunk);
    })
    // Pipe the data to the ungz stream.
    .pipe(ungz);

  // After the ungz stream has ended, verify the checksum.
  ungz
    .on(&quot;end&quot;, () =&amp;gt; {
      const expectedChecksum = checksumMap?.[pkgNameWithPlatform];
      // Skip verification if we can&apos;t find the file checksum
      if (!expectedChecksum) {
        console.warn(&quot;Skipping checksum verification&quot;);
        return;
      }
      const calculatedChecksum = hash.digest(&quot;hex&quot;);
      if (calculatedChecksum !== expectedChecksum) {
        throw errChecksum;
      }
      console.info(&quot;Checksum verified.&quot;);
    })
    // Pipe the data to the untar stream.
    .pipe(untar);

  // Wait for the untar stream to finish.
  await new Promise((resolve, reject) =&amp;gt; {
    untar.on(&quot;error&quot;, reject);
    untar.on(&quot;end&quot;, () =&amp;gt; resolve());
  });

  // Link the binaries in postinstall to support yarn
  await binLinks({
    path: path.resolve(&quot;.&quot;),
    pkg: { ...pkg, bin: { [pkg.name]: binPath } },
  });

  console.info(&quot;Installed Supabase CLI successfully&quot;);
}

await main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new part of this logic will use the files in the &lt;code&gt;lib/supabase&lt;/code&gt; directory first, and if the file does not exist, it will execute the default logic to download from &lt;code&gt;github&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After editing, we submit the changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm patch-commit &apos;/Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3&apos;
Lockfile is up to date, resolution step is skipped
Packages: +20
++++++++++++++++++++
Progress: resolved 0, reused 45, downloaded 0, added 20, done
node_modules/supabase: Running postinstall script, done in 236ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will generate a &lt;code&gt;supabase@2.34.3.patch&lt;/code&gt; file in the &lt;code&gt;patches&lt;/code&gt; directory in the project root directory.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;lib/
&lt;ul&gt;
&lt;li&gt;supabase/
&lt;ul&gt;
&lt;li&gt;supabase_2.34.3_checksums.txt&lt;/li&gt;
&lt;li&gt;supabase_darwin_arm64.tar.gz&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;patches/
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It will also generate a &lt;code&gt;patchedDependencies&lt;/code&gt; configuration in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    }
    },
    &quot;patchedDependencies&quot;: {
      &quot;supabase@2.34.3&quot;: &quot;patches/supabase@2.34.3.patch&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we move the &lt;code&gt;supabase@2.34.3.patch&lt;/code&gt; file to the &lt;code&gt;lib/supabase&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;At this time, the directory structure is as follows:&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;lib/
&lt;ul&gt;
&lt;li&gt;supabase/
&lt;ul&gt;
&lt;li&gt;supabase_2.34.3_checksums.txt&lt;/li&gt;
&lt;li&gt;supabase_darwin_arm64.tar.gz&lt;/li&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;patches/
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also change the &lt;code&gt;patchedDependencies&lt;/code&gt; configuration in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    },
    &quot;patchedDependencies&quot;: {
      &quot;supabase@2.34.3&quot;: &quot;patches/supabase@2.34.3.patch&quot;
      &quot;supabase@2.34.3&quot;: &quot;lib/supabase/supabase@2.34.3.patch&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Test changes&lt;/h2&gt;
&lt;p&gt;Delete &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, and then run &lt;code&gt;pnpm install&lt;/code&gt;, you should be able to see that the installation speed has been significantly accelerated. It only takes &lt;em&gt;266ms&lt;/em&gt; to complete the installation. Compared to the previous nearly 4 minutes, there is a very obvious improvement.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm install
 WARN  deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
 WARN  6 deprecated subdependencies found: @humanwhocodes/config-array@0.13.0, @humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0, rimraf@3.0.2
Packages: +418
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 WARN  Failed to create bin at /Users/obiscr/projects/my-project/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/projects/my-project/node_modules/supabase/bin/supabase&apos;
Progress: resolved 433, reused 402, downloaded 0, added 418, done
 WARN  Failed to create bin at /Users/obiscr/projects/my-project/node_modules/supabase/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/projects/my-project/node_modules/supabase/bin/supabase&apos;
node_modules/supabase: Running postinstall script, done in 266ms
Done in 1m 25.2s using pnpm v10.10.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This article introduces how to speed up the &lt;code&gt;pnpm&lt;/code&gt; &lt;code&gt;postinstall&lt;/code&gt; script through the &lt;code&gt;pnpm patch&lt;/code&gt; command. I hope it is useful to you.&lt;/p&gt;
</content:encoded></item><item><title>Intellij Adaptive Color Scheme</title><link>https://obiscr.com/blog/intellij-adaptive-color-scheme</link><guid isPermaLink="true">https://obiscr.com/blog/intellij-adaptive-color-scheme</guid><description>Color of UI components adaptive to IDE theme changes.</description><content:encoded>&lt;p&gt;Intellij Adaptive Color Scheme&lt;/p&gt;
&lt;p&gt;This article describes how to make the colors of user interface components in IntelliJ adaptable to changes in the IDE theme.&lt;/p&gt;
&lt;h2&gt;Create a UI Component&lt;/h2&gt;
&lt;p&gt;Suppose we now have a component like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We set a background color for the &lt;code&gt;UIContainer&lt;/code&gt; that comes from the current &lt;strong&gt;ColorsScheme&lt;/strong&gt;&apos;s &lt;code&gt;NOTIFICATION_BACKGROUND&lt;/code&gt;,
but it&apos;s clear that in this case, when we switch themes, the color of the &lt;code&gt;UIContainer&lt;/code&gt; doesn&apos;t switch with the theme&apos;s color.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UIContainer&lt;/code&gt; color doesn&apos;t switch with the theme&apos;s color when we switch themes in this case. However, in most cases,
this may cause the UI to have similar colors and fonts, so that the displayed content is not visible.&lt;/p&gt;
&lt;p&gt;So, how can we make the background color of &lt;code&gt;UIContainer&lt;/code&gt; change automatically as the theme changes?&lt;/p&gt;
&lt;h2&gt;Adaptive to IDE theme&lt;/h2&gt;
&lt;h3&gt;Implements Interfaces&lt;/h3&gt;
&lt;p&gt;Make &lt;code&gt;UIContainer&lt;/code&gt; implements Disposable and EditorColorsListener&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {
    {... rest of code}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Add Message Event&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)

      MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(this);
      connection.subscribe(EditorColorsManager.TOPIC, this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implements globalSchemeChange&lt;/h3&gt;
&lt;p&gt;When theme changed, the &lt;strong&gt;globalSchemeChange&lt;/strong&gt; method will be called.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;currentColorsScheme&lt;/strong&gt; will be the selected theme, so we regain the &lt;code&gt;notificationColor&lt;/code&gt;
from &lt;strong&gt;currentColorsScheme&lt;/strong&gt; and reset the color for the component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    {... rest of code}

    @Override
    public void globalSchemeChange(@Nullable EditorColorsScheme currentColorsScheme) {
        Color color = currentColorsScheme == null ? scheme.getColor(notificationColor) :
                currentColorsScheme.getColor(notificationColor);
        setBackground(color);
        revalidate();
        repaint();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implements dispose&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    {... rest of code}

    @Override
    public void dispose() {
        Disposer.dispose(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Finally Code&lt;/h2&gt;
&lt;p&gt;So, The final code is as follows&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)

      MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(this);
      connection.subscribe(EditorColorsManager.TOPIC, this);
    }

    @Override
    public void globalSchemeChange(@Nullable EditorColorsScheme currentColorsScheme) {
        if (currentColorsScheme == null) {
            return;
        }
        Color color = scheme.getColor(notificationColor);
        setBackground(color);
        revalidate();
        repaint();
    }

    @Override
    public void dispose() {
        Disposer.dispose(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you switch themes, the color of the component will automatically change with the color of the theme. Hope this helps.&lt;/p&gt;
</content:encoded></item><item><title>Disposer in the IntelliJ Platform</title><link>https://obiscr.com/blog/intellij-disposer</link><guid isPermaLink="true">https://obiscr.com/blog/intellij-disposer</guid><description>This article is a case study to simplify the IntelliJ Platform to introduce the IntelliJ Platform in the disposer.</description><content:encoded>&lt;p&gt;This article is a case study to simplify the IntelliJ Platform to introduce the IntelliJ Platform in the disposer.&lt;/p&gt;
&lt;p&gt;In fact, the SDK documentation already has a more detailed description of this: &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/disposers.html&quot;&gt;Disposer and Disposable&lt;/a&gt;, before we get started, it might be a good idea to take a look here.&lt;/p&gt;
&lt;h2&gt;Scene Description&lt;/h2&gt;
&lt;p&gt;I have modelled a scenario here. There is a ToolWindow layout as follows,Here we use the &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/internal-ui-inspector.html&quot;&gt;UI Inspector&lt;/a&gt; to analyse the layout:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/toolwindow-layout.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/dispose-tool-window-layout.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A rootPanel with BorderLayout, top (North) is a Toolbar, center (Center) is a JPanel. Click ➕ will add a random code snippet to Center&apos;s JPanel.&lt;/p&gt;
&lt;p&gt;The scenario, as above, is a very simple ToolWindow with very simple functionality.&lt;/p&gt;
&lt;h2&gt;Memory leak&lt;/h2&gt;
&lt;p&gt;Here is the key part of the code snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.obiscr.template.dispose.MainPanel;
import org.jetbrains.annotations.NotNull;

public class MyToolWindowFactory implements ToolWindowFactory {
    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        ContentFactory contentFactory = ContentFactory.getInstance();
        MainPanel panel = new MainPanel(project);
        Content content = contentFactory.createContent(panel.getComponent(), &quot;Dispose&quot;, false);
        toolWindow.getContentManager().addContent(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.project.Project;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.components.panels.VerticalLayout;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

public class MainPanel {

    private final JPanel rootPanel;
    private final JPanel centerPanel;
    private final Project myProject;


    public MainPanel(Project project) {
        myProject = project;
        rootPanel = new JPanel(new BorderLayout());
        centerPanel = new JPanel(new VerticalLayout(JBUI.scale(10)));
        centerPanel.setBorder(JBUI.Borders.empty(10));

        DefaultActionGroup actionGroup = new DefaultActionGroup();
        AnAction addAction = new AnAction(() -&amp;gt; &quot;Add Item&quot;, AllIcons.General.Add) {
            @Override
            public void actionPerformed(@NotNull AnActionEvent e) {
                String text = &quot;// Code snippet at &quot; + LocalDateTime.now().
                format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;)) + &quot;\n&quot;;
                text += HelloWorld.getRandomHelloWorldCode();
                Editor editor = createEditor(myProject, text);
                centerPanel.add(editor.getComponent());
                centerPanel.revalidate();
                centerPanel.repaint();
            }
        };
        actionGroup.add(addAction);
        ActionToolbarImpl actionToolbar = new ActionToolbarImpl(&quot;MyPanelToolbar&quot;, actionGroup, true);
        actionToolbar.setTargetComponent(rootPanel);
        actionToolbar.setBorder(JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1,0,1,0));
        rootPanel.add(actionToolbar, BorderLayout.NORTH);
        rootPanel.add(centerPanel, BorderLayout.CENTER);
    }

    public JPanel getComponent() {
        return rootPanel;
    }

    private Editor createEditor(Project project, String text) {
        EditorFactory editorFactory = EditorFactory.getInstance();
        EditorEx editor = (EditorEx) editorFactory.createViewer(
                editorFactory.createDocument(text),
                project);
        // Others settings
        // ...
        return editor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we just create the Editor object, but we don&apos;t clean up the memory it occupies when the program exits.&lt;/p&gt;
&lt;p&gt;When closing the IDE, relevant exception messages are displayed (Of course, this information is also displayed in the &lt;code&gt;idea.log&lt;/code&gt;)：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/console-stacktrace.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/console-stacktrace-code.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;According to the error log, we can know that it is because the created Editor object is not released when the application is closed and there is a risk of memory overflow.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/console-code-preview.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Solving&lt;/h2&gt;
&lt;p&gt;Ok, now we already know where the problem is, the following we solve the problem, the solution is also simple, just need to close the IDE, release in this object can be.&lt;/p&gt;
&lt;p&gt;First, make MainPanel implement the &lt;strong&gt;Disposable&lt;/strong&gt; interface and override the &lt;code&gt;dispose()&lt;/code&gt; method. Then after creating &lt;code&gt;editor&lt;/code&gt;, register &lt;code&gt;Disposer&lt;/code&gt; with parent &lt;code&gt;this&lt;/code&gt; to indicate that &lt;strong&gt;the editor will be destroyed when this is disposed&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MainPanel implements Disposable {
	@Override
    public void dispose() {
        centerPanel.removeAll();
        rootPanel.removeAll();
    }

    private Editor createEditor(Project project, String text) {
        EditorEx editor = ...;
        Disposer.register(this, () -&amp;gt; EditorFactory.getInstance().releaseEditor(editor));
        return editor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition, since in this case the components are all based on Content, we register the disposer for content as MainPanel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyToolWindowFactory implements ToolWindowFactory {

    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        MainPanel panel = new MainPanel(project);
        Content content = ...;
        content.setDisposer(panel);
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The source code for ContentImpl.java looks like this, and you can see that when &lt;code&gt;content&lt;/code&gt; is destroyed, it calls &lt;code&gt;Disposer.dispose(myDisposer);&lt;/code&gt; to destroy the set myDisposer, which is the MainPanel above.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Override
  public void dispose() {
    if (myShouldDisposeContent &amp;amp;&amp;amp; myComponent instanceof Disposable) {
      Disposer.dispose((Disposable)myComponent);
    }

    if (myDisposer != null) {
      Disposer.dispose(myDisposer);
      myDisposer = null;
    }

    myFocusRequest = null;
    clearUserData();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now the whole process is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;content.dispose() --(call)--&amp;gt; panel.dispose() --(call)--&amp;gt; EditorFactory.getInstance().releaseEditor(editor);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, when &lt;code&gt;content&lt;/code&gt; is closed, the editor object is also destroyed.&lt;/p&gt;
</content:encoded></item><item><title>IntelliJ Plugin Development</title><link>https://obiscr.com/blog/intellij-plugin-development</link><guid isPermaLink="true">https://obiscr.com/blog/intellij-plugin-development</guid><description>This article introduces plugin development related content, focusing on how to deal with problems encountered...</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;This article introduces plugin development related content, focusing on how to deal with problems encountered...&lt;/p&gt;
&lt;p&gt;I believe that many plugin developers in the initial period will encounter a lot of various difficulties, but this is quite normal. So far, I have developed &lt;a href=&quot;https://plugins.jetbrains.com/organizations/obiscr&quot;&gt;a number of plugins&lt;/a&gt; during which I also encountered a lot of problems. Some are still learning, some are still being solved. If you have similar problems, then this article is worth reading.&lt;/p&gt;
&lt;p&gt;For myself, when it comes to problems, there are generally so many ways to deal with them as follows:&lt;/p&gt;
&lt;h2&gt;I - IntelliJ Platform&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/intellij-platform.html&quot;&gt;IntelliJ Platform&lt;/a&gt; Equivalent to the SDK platform documentation. Includes documentation for plugins, base platform, project model, PSI, features, testing, resources, API changes, tools, and other modules.&lt;/p&gt;
&lt;h2&gt;II - IntelliJ Community&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://intellij-support.jetbrains.com/hc/en-us/community/topics/200382555-IntelliJ-IDEA-Users&quot;&gt;IntelliJ Community&lt;/a&gt; contains a lot of questions and answers.&lt;/p&gt;
&lt;h2&gt;III - IntelliJ&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-plugins&quot;&gt;IntelliJ Plugins&lt;/a&gt; is a git repository that contains many of JetBrains&apos; own plugins, where you can find good examples for reference.&lt;/p&gt;
&lt;h2&gt;IV - IntelliJ Code Samples&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-sdk-code-samples&quot;&gt;IntelliJ Code Samples&lt;/a&gt; is a git repository where good examples can be found for reference.&lt;/p&gt;
&lt;h2&gt;V - YouTrack&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issues&quot;&gt;YouTrack&lt;/a&gt; is mainly used for bug feedback, and you can also search for relevant information here when you encounter problems.&lt;/p&gt;
&lt;h2&gt;VI - Internal Model &amp;amp; Source Code&lt;/h2&gt;
&lt;p&gt;This is a very useful feature. The internal mode can be enabled from &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/enabling-internal.html&quot;&gt;this document&lt;/a&gt;. Internal mode together with &lt;a href=&quot;https://github.com/JetBrains/intellij-community&quot;&gt;IntelliJ Community Edition source code&lt;/a&gt; can solve a lot of problems.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
I do highly recommend using this approach to solve problems. When using this approach, you need to
read the IntelliJ Community Edition source code, which can be very good to develop some habits of
source code reading source code.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;VII - Slack&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://jetbrains-platform.slack.com/&quot;&gt;Here&lt;/a&gt; you can discuss plugin development for the JetBrains IDE and Team Tools, as well as all things related to the JetBrains Marketplace. There are a lot of plugin developers as well as official developers here, and most of the issues can be solved here.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
Slack for me is generally the final option when I&apos;m at the end of my rope, and when none of the above can be solved, I look for other developers or official developers in Slack to assist with the solution.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Summaries&lt;/h2&gt;
&lt;p&gt;These are the solutions to the problems I have encountered in my own development process over the past few years. I hope it can provide a reference for developers who need it.&lt;/p&gt;
</content:encoded></item><item><title>JetBrains Git Client 2025.3 EAP</title><link>https://obiscr.com/blog/jetbrains-git-client-2025-3-eap</link><guid isPermaLink="true">https://obiscr.com/blog/jetbrains-git-client-2025-3-eap</guid><description>A complete e-commerce demo built with Next.js, Stripe Checkout, and Supabase, featuring user authentication, product management, secure payment processing, and automatic purchase tracking.</description><content:encoded>&lt;p&gt;import {
LinkCard,
} from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://lp.jetbrains.com/closed-preview-for-jetbrains-git-client&quot;
title=&quot;Git Client from JetBrains — Closed Preview&quot;
description=&quot;The Closed Preview is your chance to get early access to JetBrains Git Client.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Today, I&apos;d like to share a very special content. Recently, I was lucky enough to get a closed preview of JetBrains&apos; new GitClient product. It&apos;s worth noting that the application channel is now closed, so today&apos;s content is relatively exclusive.&lt;/p&gt;
&lt;h2&gt;Startup View&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-git-client-startup-page.png&quot; alt=&quot;jetbrains-git-client-startup-page.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;First, when we open the tool, we can choose to log in to Github or GitLab, clone a project, or open an existing project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-git-client-commit-list.png&quot; alt=&quot;jetbrains-git-client-commit-list.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After opening a project, we can see that the overall UI is still clean and many unnecessary tool windows have been removed.&lt;/p&gt;
&lt;h2&gt;Settings View&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-git-client-settings-view.png&quot; alt=&quot;jetbrains-git-client-settings-view.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Another important difference is that the plugin system is gone. This is a big difference, and I hope it can be added back later.&lt;/p&gt;
&lt;h2&gt;Merge View&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-git-client-merge.png&quot; alt=&quot;jetbrains-git-client-merge.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In terms of code merging, the overall UI and behavior are basically consistent with the IDE&apos;s built-in Git tool.&lt;/p&gt;
&lt;p&gt;{/* ## Q &amp;amp; A&lt;/p&gt;
&lt;p&gt;&amp;lt;Collapse title=&quot;Q: Does it need to pay for GitClient when it is officially released?&quot; variant=&quot;note&quot; defaultExpanded={true}&amp;gt;
&lt;strong&gt;A:&lt;/strong&gt; The development team is currently focusing on the development side of things, and whether it will be a free or subscription model upon release is unknown at this time
&amp;lt;/Collapse&amp;gt; */}&lt;/p&gt;
</content:encoded></item><item><title>JetBrains码上道：从零开始开发插件</title><link>https://obiscr.com/blog/jetbrains-live-stream</link><guid isPermaLink="true">https://obiscr.com/blog/jetbrains-live-stream</guid><description>有幸参与JetBrains 官方举办的直播活动，作为嘉宾分享插件开发经验，并且还能向其他大佬一起学习。JetBrains 码上道 | 记得预约直播哦，还有机会抽取IntelliJ IDEA一年的免费许可。欢迎预约观看。</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;有幸参与JetBrains 官方举办的直播活动，作为嘉宾分享插件开发经验，并且还能向其他大佬一起学习。&lt;/p&gt;
&lt;p&gt;JetBrains 码上道 | 记得预约直播哦，还有机会抽取IntelliJ IDEA一年的免费许可。欢迎预约观看。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;相关信息如下&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直播时间：2022年9月29日，晚上8点整。&lt;/li&gt;
&lt;li&gt;直播间：扫描下图中的二维码、&lt;a href=&quot;http://live.bilibili.com/23577528&quot;&gt;BiliBili直播间&lt;/a&gt; 或 微信视频号搜索：JetBrains。&lt;/li&gt;
&lt;li&gt;文章转载自：&lt;a href=&quot;https://blog.jetbrains.com/zh-hans/blog/&quot;&gt;JetBrains 博客官网&lt;/a&gt;，&lt;a href=&quot;https://mp.weixin.qq.com/s/77-gHDxCWvCRDX1NDdVPEA&quot;&gt;JetBrains 微信公众号&lt;/a&gt;。
&amp;lt;/Aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jb-live-poster.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;IntelliJ IDEA 作为开发者青睐的工具，不仅自身功能强大，同时也吸引了众多开发者在 IntelliJ 平台上创造各种各样的插件。这些插件高效且实用，都来自作者们对开发工作的深入理解。有了这些插件，往往能让大家的工作事半功倍，满足项目开发中的各种需求，让开发工作更加得心应手。&lt;/p&gt;
&lt;p&gt;本次直播将为大家邀请到多位中文 IntelliJ IDEA 插件作者，介绍那些广为人知且高效实用的插件。本次直播活动将分为两个部分，首先，将与大家分享如何在 IntelliJ 平台从无到有开发插件，接着我们会邀请嘉宾们一起聊聊关于插件开发的故事和体会。让我们一起来开发插件吧！&lt;/p&gt;
&lt;p&gt;本次活动将在 JetBrains 中国官方 BiliBili 频道和微信视频号同步直播，别忘了预留时间，并关注 JetBrains 微信公众号以获取第一手活动信息。&lt;/p&gt;
&lt;h2&gt;分享嘉宾&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-mashangdao.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;主持人&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/shengyou.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;范圣佑 (ShengYou)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;圣佑是 JetBrains 技术布道师，对 JetBrians 相关技术与产品也有深入的理解。作为布道师，他乐意分享自己的开发经验，帮助更多开发者提升生产力及代码质量。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;注意&quot;&amp;gt;
本次活动将在 JetBrains 视频号、BiliBili 频道同步直播。直播还设有抽奖环节，我们准备了一些 IntelliJ IDEA 个人版一年期许可证，无论你在哪个平台收看直播，均有机会参与抽奖。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;嘉宾精选插件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fast Request: https://plugins.jetbrains.com/plugin/16988&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ExcelEditor: https://plugins.jetbrains.com/plugin/18663&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MybatisCodeHelperPro: https://plugins.jetbrains.com/plugin/9837&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Code Node: https://plugins.jetbrains.com/plugin/17501&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Toolset: https://plugins.jetbrains.com/plugin/14384&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rainbow Brackets: https://plugins.jetbrains.com/plugin/10080&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jetbrains-header.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;细节信息&quot;&amp;gt;
有关本次活动的更多细节信息，请阅读原文&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.jetbrains.com/zh-hans/blog/2022/09/21/jetbrains-plugin/&quot;&gt;官方博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/77-gHDxCWvCRDX1NDdVPEA&quot;&gt;微信公众号&lt;/a&gt;
&amp;lt;/Aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JetBrains Marketplace 5 周年</title><link>https://obiscr.com/blog/jetbrains-marketplace-5-years</link><guid isPermaLink="true">https://obiscr.com/blog/jetbrains-marketplace-5-years</guid><description>大概在几天前，收到了一个包裹。 下班回家打开一看，原来是 Jetbrains 5 周年礼物。</description><content:encoded>&lt;p&gt;import { Aside, Tabs, TabItem, Card } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;开箱&lt;/h2&gt;
&lt;p&gt;大概在几天前，收到了一个包裹。 下班回家打开一看，原来是 Jetbrains 5 周年礼物。&lt;/p&gt;
&lt;p&gt;开箱之后，大概有这些东西：&lt;/p&gt;
&lt;p&gt;&amp;lt;Tabs&amp;gt;
&amp;lt;TabItem label=&quot;5周年的卡片 x1&quot;&amp;gt;
&lt;img src=&quot;../../../assets/blog/images/jb-marketplace-5-years-1-min.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;L&amp;amp;T转USB数据线 x1&quot;&amp;gt;
&lt;img src=&quot;../../../assets/blog/images/jb-marketplace-5-years-2-min.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;很精致的JetBrains胸章 x1&quot;&amp;gt;
&lt;img src=&quot;../../../assets/blog/images/jb-marketplace-5-years-4-min.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;保温杯 x1&quot;&amp;gt;
&lt;img src=&quot;../../../assets/blog/images/jb-marketplace-5-years-3-min.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;布袋 x1&quot;&amp;gt;
&lt;img src=&quot;../../../assets/blog/images/jb-marketplace-5-years-5-min.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;/TabItem&amp;gt;
&amp;lt;/Tabs&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;感谢&quot;&amp;gt;
在此：感谢所有的插件用户，感谢JetBrains！
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>JIT IntelliJ based Git Client</title><link>https://obiscr.com/blog/jit-client</link><guid isPermaLink="true">https://obiscr.com/blog/jit-client</guid><description>The Git client that comes with the JetBrains family of products is probably the best Git client I&apos;ve ever used. So I plan to maintain the Git functionality as a separate Git tool.</description><content:encoded>&lt;p&gt;import { Tabs, TabItem, Card } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;Origins&lt;/h2&gt;
&lt;p&gt;Hi guys, I&apos;m remaking this project based on &lt;a href=&quot;https://github.com/JetBrains/intellij-community&quot;&gt;intellij-commiunity&lt;/a&gt;. Only a tiny part of it is finished.&lt;/p&gt;
&lt;p&gt;I named it Jit (from IntelliJ Git / JetBrains Git) and drew the new Icon. You can open a project as in IntelliJ IDEA, and the Git ToolWindow will appear directly after you open it.&lt;/p&gt;
&lt;p&gt;It all started with an issue on YouTrack: &lt;a href=&quot;https://youtrack.jetbrains.com/issue/IDEA-152437/Make-git-client-a-standalone-app&quot;&gt;Make git client a standalone app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I think it very interesting. The Git client that comes with the JetBrains family of products is probably the best Git client I&apos;ve ever used. So I plan to maintain the Git functionality as a separate Git tool.&lt;/p&gt;
&lt;h2&gt;General idea&lt;/h2&gt;
&lt;p&gt;I removed the entire Editor section for now, keeping only some of the associated ToolWindow: (Git / Commit / Pull Requests / Terminal / Notification).&lt;/p&gt;
&lt;p&gt;It&apos;s a lot of work, and as I mentioned before, there are a lot of dependencies between modules that need to be dealt with. I&apos;m busy at work and usually have only little time for this. But seriously, I&apos;m interested in this.&lt;/p&gt;
&lt;h2&gt;Updating&lt;/h2&gt;
&lt;p&gt;It&apos;s a lot of work, and as I mentioned before, there are a lot of dependencies between modules that need to be dealt with. I&apos;m busy at work and usually have only little time for this. But seriously, I&apos;m interested in this.&lt;/p&gt;
&lt;h2&gt;Preview&lt;/h2&gt;
&lt;p&gt;Here are some preview images of what&apos;s currently available.&lt;/p&gt;
&lt;p&gt;&amp;lt;Tabs&amp;gt;
&amp;lt;TabItem label=&quot;Welcome&quot;&amp;gt;
After launching, just like IntelliJ IDEA, you can select a project to open.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/welcome.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;About&quot;&amp;gt;
In About you can see the basic information. the name, icon has been changed here.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/about.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Git&quot;&amp;gt;
The Git tool window displays basic information about the repository, branches, and commits, and you can see the details of file changes by selecting a commit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/git-view.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Commit&quot;&amp;gt;
In the Commit tool window, you can see the files to be committed and enter the commit information.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/commit-view.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Terminal&quot;&amp;gt;
It also retains the terminal tool window, where you can use git commands for more complex operations. Or other basic operations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/terminal.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Pull Requests&quot;&amp;gt;
In the Pull Request Tool window, you can see the repository&apos;s current merge requests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/pull-requests.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Notification&quot;&amp;gt;
Notifications are also kept for a better interactive experience.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../assets/blog/images/notification.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;/Tabs&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Maia - Self-hosted DAG workflow orchestration and execution service</title><link>https://obiscr.com/blog/maia</link><guid isPermaLink="true">https://obiscr.com/blog/maia</guid><description>Maia is a self-hosted DAG workflow orchestration and execution service for long-running automation—observable, debuggable, and replayable.</description><content:encoded>&lt;p&gt;import { LinkCard, Aside, CardGrid, Card } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;I’ve recently been translating some Spanish articles, more than 2,000 in total. It’s been a really demanding job. Not because it’s complicated—on the contrary, it’s quite simple, but because the workload is enormous.&lt;/p&gt;
&lt;p&gt;So I built this entire workflow engine/system to assist with my translations. I can finish a year’s worth of work in two weeks. In fact, if I wanted to, I could do it even faster.&lt;/p&gt;
&lt;p&gt;That’s how this project came to be.&lt;/p&gt;
&lt;p&gt;Now I’m planning to open-source it, and I hope it will be useful to you as well. This project is distributed under the MIT license.&lt;/p&gt;
&lt;p&gt;Oh, and the agent part is still in beta testing and isn’t stable yet—I still need to keep working on that piece.&lt;/p&gt;
&lt;p&gt;Next, let me explain this project in detail.&lt;/p&gt;
&lt;p&gt;Maia, it&apos;s a self-hosted DAG workflow orchestration and execution service for long-running automation—observable, debuggable, and replayable.&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/maia&quot;
title=&quot;Maia&quot;
description=&quot;Maia is a self-hosted DAG workflow orchestration and execution service for long-running automation—observable, debuggable, and replayable.&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Persistence&lt;/strong&gt;: state and outputs are persisted with SQLite (retained and traceable)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt;: real-time logs/event streams (SSE) with replay&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated execution&lt;/strong&gt;: optional Runner + Sandbox container isolation (recommended for production)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable&lt;/strong&gt;: each step has explicit inputs/outputs and can produce artifacts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional agent&lt;/strong&gt;: helps generate/refine workflows (opt-in)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://maia.obiscr.com&quot;
title=&quot;Maia Docs&quot;
description=&quot;The documentation here contains more details.&quot;
/&amp;gt;&lt;/p&gt;
&lt;h3&gt;Agent Orchestration&lt;/h3&gt;
&lt;p&gt;I used AI to generate a workflow that fetches RSS data and analyzes it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/agent-generate.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/agent-generate-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Runs&lt;/h3&gt;
&lt;p&gt;I use workflows to check certain information about GitHub repositories.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/runs-preview-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Jobs&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/jobs-detail-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Schedules&lt;/h3&gt;
&lt;p&gt;I can also set up schedules to trigger tasks daily—for example, fetching the top 10 Hacker News stories and sending them to my Discord bot.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/schedules-detail-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Batches&lt;/h3&gt;
&lt;p&gt;If I want, I can also trigger multiple tasks in batches and process them in parallel.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/batches-detail-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The source repo&lt;/h2&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://maia.obiscr.com&quot;
title=&quot;Maia&quot;
description=&quot;The maia source code repository.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/maia-docs&quot;
title=&quot;Maia Docs&quot;
description=&quot;The maia documentation repository.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;Finally, I hope it can be useful to you. Enjoy&lt;/p&gt;
</content:encoded></item><item><title>Next.js Stripe Supabase Starter</title><link>https://obiscr.com/blog/nextjs-stripe-supabase-starter</link><guid isPermaLink="true">https://obiscr.com/blog/nextjs-stripe-supabase-starter</guid><description>A complete e-commerce demo built with Next.js, Stripe Checkout, and Supabase, featuring user authentication, product management, secure payment processing, and automatic purchase tracking.</description><content:encoded>&lt;p&gt;import {
Steps,
Card,
FileTree,
Tabs,
TabItem,
LinkCard,
CardGrid,
} from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/nextjs-stripe-supabase-starter.git&quot;
title=&quot;nextjs-stripe-supabase-starter&quot;
description=&quot;A complete e-commerce demo built with Next.js, Stripe Checkout, and Supabase, featuring user authentication, product management, secure payment processing, and automatic purchase tracking.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://nextjs-stripe-supabase-starter.vercel.app/&quot;
title=&quot;Live Demo&quot;
description=&quot;Live demo of the Nextjs Stripe Supabase Starter&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User Authentication&lt;/strong&gt;: Email/password login with Supabase Auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 Product Categories&lt;/strong&gt;: Premium Membership, Cloud Storage, and API Access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple Pricing Options&lt;/strong&gt;: One-time payments and subscriptions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stripe Checkout Integration&lt;/strong&gt;: Secure hosted payment pages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database Integration&lt;/strong&gt;: Supabase for user data and purchase records&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Webhook Processing&lt;/strong&gt;: Automatic payment confirmation and database updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purchase Tracking&lt;/strong&gt;: Real-time purchase status from database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Responsive Design&lt;/strong&gt;: Mobile-friendly interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;: Full type safety&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tech Stack&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database&lt;/strong&gt;: Supabase (PostgreSQL)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Supabase Auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payments&lt;/strong&gt;: Stripe Checkout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Styling&lt;/strong&gt;: Tailwind CSS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment&lt;/strong&gt;: Vercel (recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Product Structure&lt;/h2&gt;
&lt;p&gt;&amp;lt;CardGrid cols={3}&amp;gt;
&amp;lt;Card title=&quot;Premium Membership&quot;&amp;gt;
- &lt;strong&gt;Monthly Plan&lt;/strong&gt;: $29.99
- &lt;strong&gt;Yearly Plan&lt;/strong&gt;: $99.99
- &lt;strong&gt;Lifetime Plan&lt;/strong&gt;: $199.99
&amp;lt;/Card&amp;gt;
&amp;lt;Card title=&quot;Cloud Storage&quot;&amp;gt;
- &lt;strong&gt;50GB Storage&lt;/strong&gt;: $9.99
- &lt;strong&gt;100GB Storage&lt;/strong&gt;: $19.99
- &lt;strong&gt;500GB Storage&lt;/strong&gt;: $49.99
&amp;lt;/Card&amp;gt;
&amp;lt;Card title=&quot;API Access&quot;&amp;gt;
- &lt;strong&gt;Basic API&lt;/strong&gt;: $19.99
- &lt;strong&gt;Pro API&lt;/strong&gt;: $49.99
- &lt;strong&gt;Enterprise API&lt;/strong&gt;: $99.99
&amp;lt;/Card&amp;gt;
&amp;lt;/CardGrid&amp;gt;&lt;/p&gt;
&lt;h2&gt;Complete Setup Guide&lt;/h2&gt;
&lt;p&gt;Follow these steps to set up the project from scratch:&lt;/p&gt;
&lt;h3&gt;Step 1: Clone and Install Dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Clone the repository
git clone https://github.com/obiscr/nextjs-stripe-supabase-starter.git
cd nextjs-stripe-supabase-starter

# Install dependencies
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Create .env.local file&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;.env.local&lt;/code&gt; file in the root directory.&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nextjs-stripe-supabase-starter
&lt;ul&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.env.local&lt;/strong&gt; create this file&lt;/li&gt;
&lt;li&gt;...
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 3: Create Supabase Account&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://supabase.com&quot;&gt;Supabase&lt;/a&gt; and create a free account&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create an organization and a new project&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;strong&gt;Project overview&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;Project overview&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Copy &lt;code&gt;Project URL&lt;/code&gt; / &lt;code&gt;API Key&lt;/code&gt; into &lt;em&gt;.env.local&lt;/em&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Supabase Keys (get from Supabase Project Dashboard &amp;gt; Project Settings &amp;gt; API Keys)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;strong&gt;Project Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;API Keys&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go to &lt;strong&gt;Project Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;API Keys&lt;/strong&gt;
Copy &lt;code&gt;service_role&lt;/code&gt; key into &lt;em&gt;.env.local&lt;/em&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Supabase Keys (get from Supabase Project Dashboard &amp;gt; Project Settings &amp;gt; API Keys)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Card title=&quot;Note&quot; icon=&quot;threads&quot; &amp;gt;
Please keep &lt;code&gt;SERVICE_ROLE_KEY&lt;/code&gt; private, DO NOT share with anyone.
&amp;lt;/Card&amp;gt;
&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 4: Create Stripe Account&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://stripe.com&quot;&gt;Stripe&lt;/a&gt; and create a free account&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Complete the account setup process&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://dashboard.stripe.com&quot;&gt;Stripe Dashboard&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Navigate to &lt;strong&gt;Developers&lt;/strong&gt; &amp;gt; &lt;strong&gt;API Keys&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Copy the Key&lt;/p&gt;
&lt;p&gt;Copy your &lt;strong&gt;Publishable key&lt;/strong&gt; and &lt;strong&gt;Secret key&lt;/strong&gt; (use test keys for development) into &lt;code&gt;.env.local&lt;/code&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Stripe Keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;h3&gt;Step 5: Set Up Stripe Webhook Endpoint&lt;/h3&gt;
&lt;p&gt;&amp;lt;Tabs&amp;gt;
&amp;lt;TabItem label=&quot;Local Dev&quot;&amp;gt;
Because of local development, stripe&apos;s webhook cannot be accessed directly, so you need to use stripe cli to listen to webhook. Open terminal and run local listener.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
&amp;gt; stripe listen --forward-to localhost:3000/api/webhooks/stripe
A newer version of the Stripe CLI is available, please update to: v1.28.0
&amp;gt; Ready! You are using Stripe API Version [2022-08-01]. Your webhook signing secret is whsec_xxxxx (^C to quit)
```

You will see the webhook signing secret in the terminal. Copy **whsec_xxxxx** into **.env.local** file.

```bash collapse={2-3} ins={4}
# Get from Stripe Dashboard: https://dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Production&quot;&amp;gt;
Open Stripe Dashboard and go to Developers - Webhooks, Create a new webhook.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**API Version**: 2025.06-30.basil

**Event**: Make sure you have selected following events.

&amp;lt;FileTree&amp;gt;
  - Checkout
    - checkout.session.completed
  - Payment&amp;amp;Intent
    - payment_intent.succeeded
    - payment_intent.payment_failed
  - Product
    - product.created
    - product.updated
    - product.deleted
  - Price
    - price.created
    - price.updated
    - price.deleted
&amp;lt;/FileTree&amp;gt;

Finish the setup steps. Open webhook url and copy Signing secret **whsec_xxxxx** into **.env.local** file.

```bash collapse={2-3} ins={4}
# Get from Stripe Dashboard: https://dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;/Tabs&amp;gt;&lt;/p&gt;
&lt;h3&gt;Step 6: Initialize Supabase&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Login to Supabase&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase login
Hello from Supabase! Press Enter to open browser and login automatically.

Here is your login link in case browser did not open https://supabase.com/dashboard/cli/login?session_id=uuid&amp;amp;token_name=cli_@hostname&amp;amp;public_key=string

Enter your verification code: c4d28d97
Token cli_@hostname created successfully.

You are now logged in. Happy coding!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Init supabase&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase init
Generate VS Code settings for Deno? [y/N] N
Generate IntelliJ Settings for Deno? [y/N] N
Finished supabase init.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Link to your supabase project&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase link --project-ref your-project-id
Enter your database password (or leave blank to skip): // Enter your database password here
Connecting to remote database...
NOTICE (42P06): schema &quot;supabase_migrations&quot; already exists, skipping
Finished supabase link.
WARNING: Local config differs from linked project. Try updating supabase/config.toml
diff supabase/config.toml your-project-id
--- supabase/config.toml
+++ your-project-id
@@ -54,8 +54,8 @@

[auth]
enabled = true
-site_url = &quot;http://127.0.0.1:3000&quot;
-additional_redirect_urls = [&quot;https://127.0.0.1:3000&quot;]
+site_url = &quot;http://localhost:3000&quot;
+additional_redirect_urls = []
jwt_expiry = 3600
enable_refresh_token_rotation = true
refresh_token_reuse_interval = 10
@@ -96,9 +96,9 @@
[auth.email]
enable_signup = true
double_confirm_changes = true
-enable_confirmations = false
+enable_confirmations = true
secure_password_change = false
-max_frequency = &quot;1s&quot;
+max_frequency = &quot;1m0s&quot;
otp_length = 6
otp_expiry = 3600
[auth.email.template]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apply migrations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase db push
Connecting to remote database...
Do you want to push these migrations to the remote database?
• 20250710080949_init.sql

[Y/n] Y
NOTICE (42P06): schema &quot;supabase_migrations&quot; already exists, skipping
NOTICE (42P07): relation &quot;schema_migrations&quot; already exists, skipping
NOTICE (42701): column &quot;statements&quot; of relation &quot;schema_migrations&quot; already exists, skipping
NOTICE (42701): column &quot;name&quot; of relation &quot;schema_migrations&quot; already exists, skipping
Applying migration 20250710080949_init.sql...
NOTICE (42710): extension &quot;uuid-ossp&quot; already exists, skipping
Finished supabase db push.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;h3&gt;Step 7: Initialize Stripe Products&lt;/h3&gt;
&lt;p&gt;Run the automated script to create products and prices in your Stripe account:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npm run stripe:init

# You will see the following output.

&amp;gt; nextjs-stripe-app@0.1.0 stripe:init
&amp;gt; ts-node scripts/stripe-init.ts

(node:76080) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:76080) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///Users/obiscr/projects/nextjs-stripe-supabase-starter/scripts/stripe-init.ts is not specified and it doesn&apos;t parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add &quot;type&quot;: &quot;module&quot; to /Users/obiscr/projects/nextjs-stripe-supabase-starter/package.json.
[dotenv@17.2.0] injecting env (7) from .env.local (tip: ⚙️  load multiple .env files with { path: [&apos;.env.local&apos;, &apos;.env&apos;] })
🚀 Starting Stripe initialization...


📋 Processing product: Premium Membership
──────────────────────────────────────────────────
📦 Creating product: Premium Membership
✅ Product created: ************** - Premium Membership
💰 Creating price: Premium Monthly - $29.99
✅ Price created: ************** - Premium Monthly
💰 Creating price: Premium Yearly - $99.99
✅ Price created: ************** - Premium Yearly
💰 Creating price: Premium Lifetime - $199.99
✅ Price created: ************** - Premium Lifetime
✨ Completed Premium Membership with 3 prices

📋 Processing product: Cloud Storage
──────────────────────────────────────────────────
📦 Creating product: Cloud Storage
✅ Product created: ************** - Cloud Storage
💰 Creating price: 50GB Storage - $9.99
✅ Price created: ************** - 50GB Storage
💰 Creating price: 100GB Storage - $19.99
✅ Price created: ************** - 100GB Storage
💰 Creating price: 500GB Storage - $49.99
✅ Price created: ************** - 500GB Storage
✨ Completed Cloud Storage with 3 prices

📋 Processing product: API Access
──────────────────────────────────────────────────
📦 Creating product: API Access
✅ Product created: ************** - API Access
💰 Creating price: Basic API - $19.99
✅ Price created: ************** - Basic API
💰 Creating price: Pro API - $49.99
✅ Price created: ************** - Pro API
💰 Creating price: Enterprise API - $99.99
✅ Price created: ************** - Enterprise API
✨ Completed API Access with 3 prices

🎉 Stripe initialization completed successfully!

📊 Summary:
────────────────────────────────────────────────────────────

🏷️  Premium Membership (**************)
   💰 Premium Monthly: ************** - $29.99
   💰 Premium Yearly: ************** - $99.99
   💰 Premium Lifetime: ************** - $199.99

🏷️  Cloud Storage (**************)
   💰 50GB Storage: ************** - $9.99
   💰 100GB Storage: ************** - $19.99
   💰 500GB Storage: ************** - $49.99

🏷️  API Access (**************)
   💰 Basic API: ************** - $19.99
   💰 Pro API: ************** - $49.99
   💰 Enterprise API: ************** - $99.99

✅ You can now run your application and see the products!
🔗 Stripe Dashboard: https://dashboard.stripe.com/products
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create all the products and prices defined in the configuration. You can check them in your Stripe Dashboard.&lt;/p&gt;
&lt;p&gt;At this point, you can see the webhook trigger in stripe webhook dashboard.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A newer version of the Stripe CLI is available, please update to: v1.28.0
&amp;gt; Ready! You are using Stripe API Version [2022-08-01]. Your webhook signing secret is whsec_xxxxx (^C to quit)
2025-07-10 21:44:51   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:52   --&amp;gt; plan.created [evt_******************]
2025-07-10 21:44:52  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:52   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:53  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:53   --&amp;gt; plan.created [evt_******************]
2025-07-10 21:44:53  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:53   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:54   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:54   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:55  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:55   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:56  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:56   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:57   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:57  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:58  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:59   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:59  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:45:00   --&amp;gt; price.created [evt_******************]
2025-07-10 21:45:00  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:45:01  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything goes well, you will see the product and product item data in supabase.&lt;/p&gt;
&lt;h3&gt;Step 8: Generate data types&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npm run supabase:gen-types

&amp;gt; nextjs-stripe-app@0.1.0 supabase:gen-types
&amp;gt; supabase gen types typescript --linked &amp;gt; lib/database.types.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 9: Start the Development Server&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt; in your browser.&lt;/p&gt;
&lt;h2&gt;Finish the complete purchase flow&lt;/h2&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Register a new user&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &quot;Login&quot; button,&lt;/li&gt;
&lt;li&gt;switch to &quot;Register&quot; tab&lt;/li&gt;
&lt;li&gt;Create a new account with email and password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/nextjs-stripe-supabase-starter-login.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Browse products&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View the 3 product categories&lt;/li&gt;
&lt;li&gt;Each product has multiple pricing tiers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/nextjs-stripe-supabase-starter-items.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make a test purchase&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &quot;Buy Now&quot; on any product&lt;/li&gt;
&lt;li&gt;You&apos;ll be redirected to Stripe Checkout&lt;/li&gt;
&lt;li&gt;Use test card number: &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use any future expiry date and CVC&lt;/li&gt;
&lt;li&gt;Complete the payment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/nextjs-stripe-supabase-starter-buy.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Payment successful&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&apos;ll be redirected to success page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/nextjs-stripe-supabase-starter-finish.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify purchase&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Return to homepage&lt;/li&gt;
&lt;li&gt;The purchased item should show &quot;Purchased&quot; status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/nextjs-stripe-supabase-starter-purchased.png&quot; alt=&quot;&quot; /&gt;
&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is a simple example of how to use Stripe with Supabase. You can use this as a starting point for your own project.&lt;/p&gt;
&lt;p&gt;You can find the source code at:&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/nextjs-stripe-supabase-starter.git&quot;
title=&quot;nextjs-stripe-supabase-starter&quot;
description=&quot;A complete e-commerce demo built with Next.js, Stripe Checkout, and Supabase, featuring user authentication, product management, secure payment processing, and automatic purchase tracking.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://nextjs-stripe-supabase-starter.vercel.app/&quot;
title=&quot;Live Demo&quot;
description=&quot;Live demo of the Nextjs Stripe Supabase Starter&quot;
/&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Obfuscating Your Plugins with ProGuard</title><link>https://obiscr.com/blog/obfuscate-plugin-with-proguard</link><guid isPermaLink="true">https://obiscr.com/blog/obfuscate-plugin-with-proguard</guid><description>This article will show you a step-by-step demonstration of obfuscating your plugin with ProGuard, starting with a real example.</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;The plugin obfuscation has been officially described in great detail, see: &lt;a href=&quot;https://plugins.jetbrains.com/docs/marketplace/obfuscate-the-plugin.html&quot;&gt;obfuscate the plugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So without further ado, this post will be a step-by-step demonstration of obfuscating your plugin with ProGuard, starting with a real-world example.&lt;/p&gt;
&lt;p&gt;To see the comparison file, the key code of ProGuard obfuscation is added to this &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/commit/a55a6b643351456a67dd036ca2496a73753519a1&quot;&gt;commit&lt;/a&gt;, all the code for this post in this &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate&quot;&gt;repository&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Added ProGuard&lt;/h2&gt;
&lt;p&gt;In &lt;strong&gt;build.gradle.kts&lt;/strong&gt;, add the &lt;code&gt;buildscript&lt;/code&gt; node to import the &lt;code&gt;proguard&lt;/code&gt; dependency&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buildscript {
    repositories {
        maven {
            setUrl(&quot;https://maven.aliyun.com/repository/public/&quot;)
            setUrl(&quot;https://maven.aliyun.com/nexus/content/groups/public/&quot;)
            setUrl(&quot;https://plugins.gradle.org/m2/&quot;)
            setUrl(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;)
        }
        mavenCentral()
        gradlePluginPortal()
    }

    dependencies {
        classpath(&quot;com.guardsquare:proguard-gradle:7.3.2&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Configuring ProGuard&lt;/h2&gt;
&lt;p&gt;Register a new &lt;code&gt;task&lt;/code&gt; in &lt;strong&gt;build.gradle.kts&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // Register a new task, task name is &quot;proguard&quot;
    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        dependsOn(instrumentedJar)
        verbose()

        val javaHome = System.getProperty(&quot;java.home&quot;)
        File(&quot;$javaHome/jmods/&quot;).listFiles()!!.forEach { libraryjars(it.absolutePath)}

        // Use the jar task output as a input jar. This will automatically add the necessary task dependency.
        injars(&quot;build/libs/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;)
        outjars(&quot;build/obfuscated/output/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;)


        libraryjars(configurations.compileClasspath.get())

        dontshrink()
        dontoptimize()

        adaptclassstrings(&quot;**.xml&quot;)
        adaptresourcefilecontents(&quot;**.xml&quot;)

        // Allow methods with the same signature, except for the return type,
        // to get the same obfuscation name.
        overloadaggressively()

        // Put all obfuscated classes into the nameless root package.
        repackageclasses(&quot;&quot;)
        dontwarn()

        printmapping(&quot;build/obfuscated/output/${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}-ProGuard-ChangeLog.txt&quot;)

        target(properties(&quot;pluginVersion&quot;))

        adaptresourcefilenames()
        optimizationpasses(9)
        allowaccessmodification()

        keepattributes(&quot;Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod&quot;)

        keep(&quot;&quot;&quot;
            class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
             &quot;&quot;&quot;.trimIndent()
        )

        keepclassmembers(&quot;&quot;&quot;
            class * {public static ** INSTANCE;}
             &quot;&quot;&quot;.trimIndent()
        )
        keep(&quot;class com.intellij.util.* {*;}&quot;)
    }


    prepareSandbox {
        if (properties(&quot;enableProGuard&quot;).toBoolean()) {
            dependsOn(&quot;proguard&quot;)
            pluginJar.set(File(&quot;build/obfuscated/output/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;))
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
These are the syntax of ProGuard, for more rules see: &lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;Official ProGuard Documentation&lt;/a&gt;, we focus on the last &lt;code&gt;keep&lt;/code&gt; and &lt;code&gt;keepclassmembers&lt;/code&gt;.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Build test&lt;/h2&gt;
&lt;p&gt;After the configuration is complete, run the plugin and the debug message is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;21:40:12: Executing &apos;runIde&apos;...

Starting Gradle Daemon...
Connected to the target VM, address: &apos;127.0.0.1:12563&apos;, transport: &apos;socket&apos;
Gradle Daemon started in 915 ms
&amp;gt; Task :initializeIntelliJPlugin
&amp;gt; Task :patchPluginXml UP-TO-DATE

&amp;gt; Task :verifyPluginConfiguration
[gradle-intellij-plugin :verifyPluginConfiguration] The following plugin configuration issues were found:
- The Java configuration specifies sourceCompatibility=11 but IntelliJ Platform 2022.3.3 requires sourceCompatibility=17.
See: https://jb.gg/intellij-platform-versions

&amp;gt; Task :compileKotlin NO-SOURCE
&amp;gt; Task :compileJava UP-TO-DATE
&amp;gt; Task :processResources UP-TO-DATE
&amp;gt; Task :classes UP-TO-DATE
&amp;gt; Task :setupInstrumentCode
&amp;gt; Task :instrumentCode UP-TO-DATE
&amp;gt; Task :jar UP-TO-DATE
&amp;gt; Task :inspectClassesForKotlinIC UP-TO-DATE
&amp;gt; Task :instrumentedJar UP-TO-DATE
&amp;gt; Task :proguard &amp;lt;=== proguard task 已经成功执行。
&amp;gt; Task :prepareSandbox
Disconnected from the target VM, address: &apos;127.0.0.1:12563&apos;, transport: &apos;socket&apos;
Connected to the target VM, address: &apos;localhost:12616&apos;, transport: &apos;socket&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Obfuscated results&lt;/h2&gt;
&lt;p&gt;After obfuscated:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/obfucated.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On the left side, file names have been obfuscated, and on the right side, file class names, method names, and variable names have been obfuscated.&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
This is just a very basic obfuscation effect, if you need more complex. You can refer to the &lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;ProGuard official documentation&lt;/a&gt; mentioned above.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Special circumstances&lt;/h2&gt;
&lt;p&gt;In IntelliJ plugin projects, there are some files that are not to be obfuscated. Two cases are listed in this demo.&lt;/p&gt;
&lt;h3&gt;Called via reflection&lt;/h3&gt;
&lt;p&gt;One of the files in the project is &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/blob/main/src/main/java/com/obiscr/template/MyDefaultFileType.java&quot;&gt;MyDefaultFileType.java&lt;/a&gt;. This file adds a new file type, let&apos;s call it a J-file, with a &lt;code&gt;*.j&lt;/code&gt; or &lt;code&gt;*.J&lt;/code&gt; file extension.&lt;/p&gt;
&lt;p&gt;This file has already been registered in plugin.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;extensions defaultExtensionNs=&quot;com.intellij&quot;&amp;gt;
    ...
    &amp;lt;fileType name=&quot;MyNativeFile&quot; implementationClass=&quot;com.obiscr.template.MyDefaultFileType&quot; fieldName=&quot;INSTANCE&quot;
              extensions=&quot;j;J&quot; order=&quot;first&quot;/&amp;gt;
    ...
&amp;lt;/extensions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key point here is a fieldName attribute: &lt;strong&gt;INSTANCE&lt;/strong&gt;, which corresponds to &lt;strong&gt;INSTANCE&lt;/strong&gt; in &lt;code&gt;MyDefaultFileType.java&lt;/code&gt;. In other words, they have to be the same. Otherwise the property will not be found when the call is reflected off.&lt;/p&gt;
&lt;p&gt;For example, once we&apos;ve removed the proguard task&apos;s&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        ...

        // keepclassmembers(&quot;&quot;&quot;
        //     class * {public static ** INSTANCE;}
        //      &quot;&quot;&quot;.trimIndent()
        // )
        ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/INSTANCE-obfucated.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Run the plugin and you get an error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2023-09-24 21:31:18,647 [   3018]   WARN - #c.i.e.RunManager - Must be not called before project components initialized
Info  | RdCoroutineScope          | 53:DefaultDispatcher-worker-35 | RdCoroutineHost overridden
2023-09-24 21:31:21,580 [   5951] SEVERE - #c.i.o.f.i.FileTypeManagerImpl - INSTANCE
java.lang.NoSuchFieldException: INSTANCE
    at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
    at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.instantiateFileTypeBean(FileTypeManagerImpl.java:492)
    at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.mergeOrInstantiateFileTypeBean(FileTypeManagerImpl.java:463)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It shows that the &lt;strong&gt;INSTANCE&lt;/strong&gt; attribute could not be found. So for this case, you can use proguard&apos;s keepclassmembers to keep the &lt;strong&gt;INSTANCE&lt;/strong&gt; attribute from being obfuscated in all classes. Of course, you can also keep INSTANCE from being obfuscated in all classes that implements INativeFileType if you want. This is a more precise control.&lt;/p&gt;
&lt;p&gt;You may ask, since this problem can be avoided by keeping the fieldName in plugin.xml the same as &lt;strong&gt;INSTANCE&lt;/strong&gt; in &lt;code&gt;MyDefaultFileType.java&lt;/code&gt;, wouldn&apos;t it be possible to avoid this problem by changing it to the same value?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/INSTANCE-keep-it.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Yes, this is true in principle. However, the obfuscation generates a random variable name each time. That is, the attribute fieldName must have been specified as a certain exact value first, and then the packaging started, and in the class file after packaging, the INSTANCE is not necessarily the value specified above. Therefore, we still need to exclude &lt;strong&gt;INSTANCE&lt;/strong&gt; from the obfuscation rules to avoid this problem.&lt;/p&gt;
&lt;h3&gt;Local data storage&lt;/h3&gt;
&lt;p&gt;In many cases, there is a need to store some data locally, such as settings data, environment data, etc. In this case, you need to use @State to specify a file to store the data. In this case, you need to use &lt;code&gt;@State&lt;/code&gt; to specify the storage file. For example, the&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.obiscr.template;

import com.intellij.openapi.components.*;
import com.intellij.openapi.project.Project;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@State( name = &quot;com.obiscr.template.MyState&quot;, storages = @Storage(&quot;my-state/state.xml&quot;))
public class MyState implements PersistentStateComponent&amp;lt;MyState&amp;gt; {
    public String currentVersion = &quot;&quot;;
    public Boolean enableFeature = true;
    public String myCustomKey = &quot;&quot;;

    public static MyState getInstance(@NotNull Project project) {
        return project.getService(MyState.class);
    }

    @Nullable
    @Override
    public MyState getState() {
        return this;
    }

    @Override
    public void loadState(@NotNull MyState state) {
        XmlSerializerUtil.copyBean(state, this);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here a storage file and some properties are defined, it will and a new &lt;code&gt;state.xml&lt;/code&gt; file is created in the my-state directory of the project&apos;s .idea directory, with the following contents&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project version=&quot;4&quot;&amp;gt;
  &amp;lt;component name=&quot;com.obiscr.template.MyState&quot;&amp;gt;
    &amp;lt;option name=&quot;currentVersion&quot; value=&quot;v1.0.1&quot; /&amp;gt;
    &amp;lt;option name=&quot;enableFeature&quot; value=&quot;false&quot; /&amp;gt;
    &amp;lt;option name=&quot;myCustomKey&quot; value=&quot;value&quot; /&amp;gt;
  &amp;lt;/component&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the same way, the name attribute in option here corresponds to the three attributes in &lt;code&gt;MyState.java&lt;/code&gt;. These attributes should not be obfuscated; if they are, the original data will not be read. For example, if you set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;currentVersion： v1.0.1&lt;/li&gt;
&lt;li&gt;enableFeature：false&lt;/li&gt;
&lt;li&gt;myCustomKey：value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After obfuscation, assume the following properties in MyState.java:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;currentVersion -&amp;gt; a&lt;/li&gt;
&lt;li&gt;enableFeature -&amp;gt; d&lt;/li&gt;
&lt;li&gt;myCustomKey -&amp;gt; c&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The data in state.xml cannot be read at this point. Because there is no option name is: a, d, c data, so the final value read out is the default value defined in MyState.java, once this value has changed, will be in the xml again to add a new attribute. name is the variable name after the obfuscation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now the currentVersion has been obfuscated to &lt;code&gt;a&lt;/code&gt;, so if the value of currentVersion changes, change it to &lt;code&gt;v1.0.2&lt;/code&gt;, then &lt;code&gt;state.xml&lt;/code&gt; will look like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project version=&quot;4&quot;&amp;gt;
  &amp;lt;component name=&quot;com.obiscr.template.MyState&quot;&amp;gt;
    &amp;lt;option name=&quot;currentVersion&quot; value=&quot;v1.0.1&quot; /&amp;gt;
    &amp;lt;option name=&quot;enableFeature&quot; value=&quot;false&quot; /&amp;gt;
    &amp;lt;option name=&quot;myCustomKey&quot; value=&quot;value&quot; /&amp;gt;
    &amp;lt;!-- Added --&amp;gt;
    &amp;lt;option name=&quot;a&quot; value=&quot;v1.0.2&quot; /&amp;gt;
  &amp;lt;/component&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;&amp;lt;option name=&quot;a&quot; value=&quot;v1.0.2&quot; /&amp;gt;&lt;/code&gt; will be added. So as long as the variable name is not the same after obfuscation, it will keep adding. So here&apos;s a way to fix it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        ...

        keep(&quot;&quot;&quot;
            class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
             &quot;&quot;&quot;.trimIndent()
        )
        ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This rule ignores all classes that implement PersistentStateComponent.&lt;/p&gt;
&lt;h2&gt;Summaries&lt;/h2&gt;
&lt;p&gt;This article briefly describes how to use ProGuard and obfuscate the IntlliJ plugin and integrate it into a gradle task.&lt;/p&gt;
&lt;p&gt;Actually, I prefer to call it a framework. The specific obfuscation rules will need to be customised for your own project. I hope it can help you.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This is just a demo project for research and study, please use it in your project at your own discretion.&lt;/p&gt;
&lt;p&gt;I will not be responsible for any kind of damage caused by using this obfuscation scheme.&lt;/p&gt;
&lt;h2&gt;Useful Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate&quot;&gt;Code Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/commit/a55a6b643351456a67dd036ca2496a73753519a1&quot;&gt;Key commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;ProGuard documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/marketplace/obfuscate-the-plugin.html&quot;&gt;JetBrains Obfuscate the plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/persistence.html&quot;&gt;Persistence data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/language-and-filetype.html&quot;&gt;Add new file type&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>关于OpenAI封号的测试结果</title><link>https://obiscr.com/blog/openai-block-account</link><guid isPermaLink="true">https://obiscr.com/blog/openai-block-account</guid><description>最近OpenAI封号很厉害，传言中，各种原因都有，因此来测试一下。</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;我的账号没有一个被封的。&lt;/p&gt;
&lt;h2&gt;基本条件&lt;/h2&gt;
&lt;p&gt;一共准备了 24 个账号（&lt;strong&gt;21&lt;/strong&gt;个老帐号，&lt;strong&gt;3&lt;/strong&gt;个新账号）。全部都是使用邮箱注册的。首先保证网络能够正常访问，这个不必多言。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了测试结果尽可能准确，我使用了多个邮箱后缀的。在这批账号里面，至少有&lt;strong&gt;8&lt;/strong&gt;种邮箱域名。&lt;/p&gt;
&lt;p&gt;有些账号是只用过API Key，没用过网页版ChatGPT。有些账号两者都有。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;提示&quot;&amp;gt;
下面数据做了脱敏处理。使用额度是3月29号的时候通过API自动刷新的，这个无伤大雅。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;账号&lt;/th&gt;
&lt;th&gt;使用额度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;q*********@b***.com&lt;/td&gt;
&lt;td&gt;17.3866&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t*********@j***.com&lt;/td&gt;
&lt;td&gt;16.89172&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;l*********@z***.com&lt;/td&gt;
&lt;td&gt;16.8178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;f*********@e***.com&lt;/td&gt;
&lt;td&gt;16.6888&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x*********@d***.com&lt;/td&gt;
&lt;td&gt;16.68422&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v*********@s***.com&lt;/td&gt;
&lt;td&gt;16.63994&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a*********@f***.com&lt;/td&gt;
&lt;td&gt;16.52996&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4*********@e***.com&lt;/td&gt;
&lt;td&gt;16.5265&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p*********@e***.com&lt;/td&gt;
&lt;td&gt;16.49332&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;strong&gt;&lt;strong&gt;*&lt;/strong&gt;&lt;/strong&gt;@c***.com&lt;/td&gt;
&lt;td&gt;16.30722&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;w*********@d***.com&lt;/td&gt;
&lt;td&gt;16.29588&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a*********@d***.com&lt;/td&gt;
&lt;td&gt;16.0017&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s*********@f***.com&lt;/td&gt;
&lt;td&gt;15.82358&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;strong&gt;&lt;strong&gt;*&lt;/strong&gt;&lt;/strong&gt;@b***.com&lt;/td&gt;
&lt;td&gt;10.73738&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b*********@s***.com&lt;/td&gt;
&lt;td&gt;7.83864&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t*********@v***.com&lt;/td&gt;
&lt;td&gt;7.42252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s*********@d***.com&lt;/td&gt;
&lt;td&gt;7.29326&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v*********@d***.com&lt;/td&gt;
&lt;td&gt;7.17466&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v*********@h***.com&lt;/td&gt;
&lt;td&gt;7.17252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9*********@n***.com&lt;/td&gt;
&lt;td&gt;7.17116&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2*********@a***.com&lt;/td&gt;
&lt;td&gt;7.0543&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;y*********@p***.com&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1*********@b***.com&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p*********@f***.com&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;开始测试&lt;/h2&gt;
&lt;p&gt;第一轮：登录测试，全部通过。&lt;/p&gt;
&lt;p&gt;第二轮：全部使用中文问答。（&lt;em&gt;数据太多，只有部分截图&lt;/em&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中，在登录了几个账号以后，出现了Cloudflare的验证。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image6.png&quot; alt=&quot;image6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;中途出现过一次不可用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image4.png&quot; alt=&quot;image4&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;很抱歉一次都没有出现封号。在现在流传的一些封号原因里面：&lt;/p&gt;
&lt;p&gt;（1）使用中文：个人感觉应该不会。&lt;/p&gt;
&lt;p&gt;（2）OpenAI政策：还有一种原因，问的问题违反了OpenAI的相关政策。这也很有可能导致封号。OpenAI有专门的审核机制，具体见：&lt;a href=&quot;https://platform.openai.com/docs/guides/moderation&quot;&gt;moderation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;（3）关于购买的账号被封，我有一个很大胆的想法：你购买的账号可能根本不是你一人在使用。除非你自己修改过密码，能排除这个因素。&lt;/p&gt;
&lt;p&gt;（4）使用第三方服务。这个我之前在群里说过。应用户要求，我的插件最近也支持了第三方配置。还附加了安全性说明。使用第三方的一定要确保安全性。比如你填写的key，有没有被服务商收集。做成一个公共的Key的池子。被它所服务的其他用户使用。事实上，你无从得知。从技术的角度来讲，这非常容易实现。毫无难度。&lt;/p&gt;
&lt;p&gt;顺便提一下如果用自己的Key使用第三方服务，请特别注意这一点。&lt;/p&gt;
&lt;p&gt;（5）邮箱权重：我使用的这些账号，邮箱域名基本上是像验证码一样的，毫无规则可言。上面仅仅只测试了一小部分。我还有很多新账号也是如此。邮箱域名就像验证码。可以说权重非常非常低。但是也没有被封。因此对于这点，个人感觉应该不会。&lt;/p&gt;
&lt;p&gt;（6）IP问题：这个我没办法测试。但是可能性非常大。IP滥用，DNS污染都有可能导致这个问题。&lt;/p&gt;
&lt;p&gt;（7）不要把自己的账号分享给别人。找人代充Plus一定要找信得过的。&lt;/p&gt;
&lt;p&gt;（8）其他...&lt;/p&gt;
&lt;p&gt;当然不排除另外一种情况，封号并不是实时的。有可能（&lt;em&gt;而且可能性很大&lt;/em&gt;）是通过延时任务来处理。所以现在测试正常，指不定明天就封了。这也是一种可能。&lt;/p&gt;
&lt;p&gt;另外关于这次测试，也有不准确的地方，因为一些条件限制，我无法模拟对应的环境。有什么地方说错，还请提出批评。谢谢！&lt;/p&gt;
&lt;p&gt;这些账号，我后面应该会跟进，看会不会被封。不过不保证（手动狗头），太忙了。实在腾不出时间。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;ChatGPT 随机说了一句话，我感觉挺不错的，就顺便贴上来，与诸君共勉。（&lt;em&gt;虽然可能原作者不是它，但相遇即是缘&lt;/em&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/image7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;AI的话语&quot;&amp;gt;
在这个充满挑战和机遇的世界，我们每个人都有着自己的故事和人生轨迹。无论是成功的喜悦，还是失败的痛苦，每一个经历都值得我们好好铭记和反思。生命如同一条漫长的河流，我们需要学会适应各种风浪和流程，才能不断前行。当我们遇到困难时，不要轻易放弃，要勇敢面对并寻求解决方案。同时，也要珍惜每一个值得感恩的人和时刻，保持乐观的态度和积极的心态，才能在人生的旅途中走得更加坚定和美好。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>ResoLine App - Your professional screen resolution assistant tool</title><link>https://obiscr.com/blog/resoline-v1-0-1</link><guid isPermaLink="true">https://obiscr.com/blog/resoline-v1-0-1</guid><description>ResoLine is a professional screen resolution assistant tool to help you visually preview and compare the actual display of different resolutions on your screen.</description><content:encoded>&lt;p&gt;import { LinkCard } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;ResoLine is a professional screen resolution assistant tool to help you visually preview and compare the actual display of different resolutions on your screen.&lt;/p&gt;
&lt;h2&gt;Install Resoline&lt;/h2&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://apps.apple.com/cn/app/resoline/id6744457578&quot;
title=&quot;App Store&quot;
description=&quot;Download from App Store&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;h3&gt;Localization&lt;/h3&gt;
&lt;p&gt;Simplified Chinese and English are currently supported.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/appearance.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/preference.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Customize the look&lt;/h3&gt;
&lt;p&gt;You can customize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For lines: line type / color / thickness.&lt;/li&gt;
&lt;li&gt;For preview text: font color / background color / transparency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Custom resolutions&lt;/h3&gt;
&lt;p&gt;The system already has some common resolutions built in, you can also add custom resolutions as needed.&lt;/p&gt;
</content:encoded></item><item><title>How to design an efficient sign-in system</title><link>https://obiscr.com/blog/sign-in</link><guid isPermaLink="true">https://obiscr.com/blog/sign-in</guid><description>Designing an Efficient sign-in System by Bitwise。</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;Suppose there is a monthly check-in activity now, and you want to record the monthly check-in data of users. If it was you, how would you design this feature?&lt;/p&gt;
&lt;p&gt;In Java, a variable of type &lt;code&gt;int&lt;/code&gt; occupies 4 bytes of space, and a byte has 8 bits (bit).&lt;/p&gt;
&lt;p&gt;In other words, a variable of type int will take up 32b(it) of space. The value range of int: -2&amp;lt;sup&amp;gt;31&amp;lt;/sup&amp;gt; ~ 2&amp;lt;sup&amp;gt;31&amp;lt;/sup&amp;gt;-1 (-2147483648~2147483647)&lt;/p&gt;
&lt;p&gt;The maximum number of months in each month is 31 days. In February, basically &amp;lt;sup&amp;gt;[1]&amp;lt;/sup&amp;gt;28 days in normal years and 29 days in leap years. They are all less than 32 (the 32 bits mentioned above).&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tip&quot;&amp;gt;
[1] There have been monthly fluctuations in history
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Think about it, is it possible to use each bit of an int to store the check-in status of a certain day?&lt;/p&gt;
&lt;p&gt;As shown in the figure below, assuming that there is a number 0 below, after converting it into binary, there are 32 0s, from the lowest digit to the highest digit, each number represents the check-in situation of a certain day:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 31th      26th      21th      16th      11th      6th      First
  ^         ^         ^         ^         ^         ^         ^
  |         |         |         |         |         |         |
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;E.g:&lt;/p&gt;
&lt;p&gt;There is such an int number: 13030250, which is converted to binary as follows:&lt;/p&gt;
&lt;p&gt;13030250（decimal）= 00000000 00&lt;strong&gt;11&lt;/strong&gt;00&lt;strong&gt;1&lt;/strong&gt;0 0&lt;strong&gt;11&lt;/strong&gt;0&lt;strong&gt;1&lt;/strong&gt;0&lt;strong&gt;1&lt;/strong&gt;0 0&lt;strong&gt;11&lt;/strong&gt;0&lt;strong&gt;1&lt;/strong&gt;0&lt;strong&gt;1&lt;/strong&gt;0（binary）&lt;/p&gt;
&lt;p&gt;First of all, let&apos;s set the rules, binary numbers, if a digit is &amp;lt;b&amp;gt;0&amp;lt;/b&amp;gt;, it means that it has not checked in, and if it is &amp;lt;b&amp;gt;1&amp;lt;/b&amp;gt;, it means that it has already checked in.&lt;/p&gt;
&lt;p&gt;Combined with the converted binary numbers above (the signed-in data has been bolded). It can be seen that the 2nd, 4th, 6th, 7th, 10th, 12th, 14th, 15th, 18th, 21st, and 22nd days have already signed in (because the numbers in these digits are all 1), and the remaining days have not signed in .&lt;/p&gt;
&lt;p&gt;When signing in, just change the number in the corresponding position to &lt;code&gt;1&lt;/code&gt;. To put it in layman&apos;s terms, if you sign in on the first day, change the number in the first position to 1, and if you sign in on the nth day, change the number in the nth position to 1.&lt;/p&gt;
&lt;p&gt;After the above demonstration, it can be seen that this design is completely feasible.&lt;/p&gt;
&lt;h2&gt;Coding&lt;/h2&gt;
&lt;h3&gt;Sign-in process&lt;/h3&gt;
&lt;p&gt;Before you start writing, you need to know the basic logic of bit operations. This is not complicated, so let&apos;s not talk about it. Let&apos;s talk about what is used in the project.&lt;/p&gt;
&lt;p&gt;Let&apos;s run the logic manually:&lt;/p&gt;
&lt;p&gt;Suppose there is a variable value of 3351, ie: &lt;code&gt;int value = 3351;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Convert it to binary: 3351&amp;lt;sub&amp;gt;(decimal)&amp;lt;/sub&amp;gt; = 00000000 00000000 00000111 00010111&amp;lt;sub&amp;gt;(binary)&amp;lt;/sub&amp;gt;&lt;/p&gt;
&lt;p&gt;Then let&apos;s take a look. The data of 00000000 00000000 00000111 00010111 indicates that the &lt;code&gt;1st, 2nd, 3rd, 5th, 9th, 10th, 11th day&lt;/code&gt; of this month has signed in. The rest of the day was unsigned.&lt;/p&gt;
&lt;p&gt;Then again, we want to make &amp;lt;sup&amp;gt;[2]&amp;lt;/sup&amp;gt; sign-in on the fourth day, which is equivalent to a supplementary sign. How should we change it?&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tip&quot;&amp;gt;
[2] It can be seen from this data that it is at least the 11th of a certain month, because the 11th day has already been signed. Check-in can only be done on the same day or on a day that has already passed, and cannot be signed in to a future day on that day.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;p&gt;First of all, we already know the requirements, that is, the fourth day to sign in, so that is, to operate the fourth bit of this binary data, see the following example: in order to facilitate the demonstration, the high bits of 0 are discarded&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 00000000 00000000 00000111 00010111 ---------------------------&amp;gt; 111 00011011

 ========================================= PART 1 ============================================
       lowest bit
           ^
           |
 11100010111 &amp;gt;&amp;gt; (4-1) = 11100010    # Let the nth digit itself be shifted right (&amp;gt;&amp;gt;) n-1 bits
                      ^ 00000001    # XOR(^) 1 let its lowest bit (which is the fourth bit we want) change to 1
                      ----------
                        11100011    # Finally, lowest bit has chhanged to 1, Changing to 1 also means signing in

 ========================================= PART 2 ============================================
 11100011000 = (4-1) &amp;lt;&amp;lt; 11100011    # Right to leat, Because the lower 3 bits were discarded above in order to handle the 4th digit, and are now to be restored, the left shift
|11100010111                        # an OR operation on this number after the left shift is complete
------------
 11100011111(result)                # We got the final result

 ========================================= PART 3 ============================================
 compare it：
 11100010111（initinal）
 11100011111（result）
 ------------------
        ^
        |
 4th, from 0 to 1, means un sign-in to signed-in
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four kinds of operations are used above, and the operation rules are explained below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shift left: Shift a few bits to the left, just add a few 0s at the end of the number&lt;/li&gt;
&lt;li&gt;Shift right: shift a few bits to the right and discard the lowest bits&lt;/li&gt;
&lt;li&gt;XOR operation: same as 0, different as 1&lt;/li&gt;
&lt;li&gt;Logical OR: As long as one is 1, it is 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Convert above process to code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void sign(int day_of_month)
{
    int tmp = value;
    value = ((signStorage &amp;gt;&amp;gt; (day_of_month-1)) ^ 1) &amp;lt;&amp;lt; (day_of_month-1) | tmp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Check sign-in&lt;/h2&gt;
&lt;p&gt;The sign-in has been mentioned above. Now let&apos;s talk about how to check whether there is a sign-in on a certain day.&lt;/p&gt;
&lt;p&gt;In fact, the check-in process is very similar to the check-in process, but the latter step is different.&lt;/p&gt;
&lt;p&gt;In a nutshell, the sign-in process is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
A[First move right to get the number of digits of the day you want to operate] --&amp;gt;B[Change the number 0 representing that day to 1]
    B --&amp;gt; C[left shift and OR operation restore the original data]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the check-in process is&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
A[First move right to get the number of digits of the day you want to operate] --&amp;gt;B{Determine whether the number of that day is 0 or 1}
    B --&amp;gt; |number=0| C[no sign-in]
    B --&amp;gt; |Number=1| D[Sign in]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s walk through the process in detail: Let&apos;s take the results calculated above as an example. The above calculation is: 11100011111. Now to judge whether there is a check-in on the third day, then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 11100011111 &amp;gt;&amp;gt; (3-1) = 111000111 # For the 3rd day, shift right by (3-1) bits
                      &amp;amp; 000000001 # then &amp;amp;1
                      -------------
                        000000001 == 1 # If the result is equal to 1, it means that there is a check-in on the third day
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two kinds of operations are used above, and the operation rules are explained below (repetitions will not be explained):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logical AND: The result is 1 only if both numbers are 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s take another example to determine whether there is a check-in on the 7th day:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 11100011111 &amp;gt;&amp;gt; (7-1) = 11100           # 求第7天，则右移(7-1)位
                      &amp;amp; 00001           # 然后&amp;amp;1
                  -----------
                        00000 == 0      # 结果等于0 则说明第7天没有签到
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s convert the above process into code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int checkSign(int day_of_month)
{
    if (day_of_month &amp;lt;=0)
        day_of_month = get_cur_day();
    return (signStorage &amp;gt;&amp;gt; (day_of_month-1)) &amp;amp; 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Full code&lt;/h2&gt;
&lt;h3&gt;C&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;time.h&amp;gt;

int sign(int day_of_month);

int checkSign(int day_of_month);

int get_cur_day();

void dec2bin(int n);

int signStorage = 0;

int main()
{
    printf(&quot;BEFORE sign-in:\n&quot;);
    dec2bin(signStorage);
    for(int i = 1; i&amp;lt;=32 ;i++){
        sign(i);
    }

    printf(&quot;AFTER sign-in:\n&quot;);
    dec2bin(signStorage);

    printf(&quot;\nCHECK sign-in:\n&quot;);
    for(int i = 1; i&amp;lt;=32 ;i++){
        printf(&quot;Day %d sign-in: %d\n&quot;,i, checkSign(i));
    }

    return 0;
}


int sign(int day_of_month)
{
    int tmp = signStorage;
    if (day_of_month &amp;lt;=0)
        day_of_month = get_cur_day();
    else
        signStorage = ((signStorage &amp;gt;&amp;gt; (day_of_month-1)) ^ 1) &amp;lt;&amp;lt; (day_of_month-1) | tmp;
    return 1;

}

int checkSign(int day_of_month)
{
    if (day_of_month &amp;lt;=0)
        day_of_month = get_cur_day();
    return (signStorage &amp;gt;&amp;gt; (day_of_month-1)) &amp;amp; 1;
}


int get_cur_day()
{
    time_t time_p;
    struct tm *p;
    time (&amp;amp;time_p);
    p=gmtime(&amp;amp;time_p);
    return p-&amp;gt;tm_mday;
}

void dec2bin(int n){
    int c, k;
    for (c = 31; c &amp;gt;= 0; c--)
      {
        k = n &amp;gt;&amp;gt; c;

        if (k &amp;amp; 1)
          printf(&quot;1&quot;);
        else
          printf(&quot;0&quot;);
      }
      printf(&quot;\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Java&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Calendar;

public class SignDay {

    public static int signStorage = 0;

    public static void main(String[] args) {
        System.out.println(&quot;BEFORE sign-in:&quot;);
        System.out.println(Integer.toBinaryString(signStorage));
        for(int i = 1; i&amp;lt;=32 ;i++){
            sign(i);
        }
        System.out.println(&quot;\AFTER sign-in:&quot;);
        System.out.println(Integer.toBinaryString(signStorage));

        System.out.println(&quot;\nCHECK sign-in:&quot;);
        for(int i = 1; i&amp;lt;=32 ;i++){
            boolean signed = checkSign(i);
            if (signed) {
                System.out.println(&quot;Day &quot; + i + &quot; sign-in: &quot; + signed);
            } else {
                System.err.println(&quot;Day &quot; + i + &quot; sign-in: &quot; + signed);
            }
        }
    }


    public static boolean sign(int dayOfMonth){
        int tmp = signStorage;
        if (dayOfMonth &amp;lt;=0){
            dayOfMonth = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
        }
        if (dayOfMonth % 4 !=0){
            signStorage = ((signStorage &amp;gt;&amp;gt; (dayOfMonth-1)) ^ 1) &amp;lt;&amp;lt; (dayOfMonth-1) | tmp;
        }
        // do other actions
        return false;
    }

    public static boolean checkSign(int dayOfMonth) {
        if (dayOfMonth &amp;lt;=0){
            dayOfMonth = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
        }
        return ((signStorage &amp;gt;&amp;gt; (dayOfMonth-1)) &amp;amp; 1) == 1;
    }

}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SmartKnob Firmware</title><link>https://obiscr.com/blog/smartknob-firmware</link><guid isPermaLink="true">https://obiscr.com/blog/smartknob-firmware</guid><description>This article focuses on how to write SmartKnob firmware and how to deal with some problems.</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Install &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Download &lt;a href=&quot;https://github.com/scottbez1/smartknob&quot;&gt;Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Open the &lt;strong&gt;smartknob/firmware&lt;/strong&gt; directory with VS Code. (Just open the firmware directory, not the whole smartknob directory)&lt;/p&gt;
&lt;h2&gt;Install VS Code Extensions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Better C++ Syntax&lt;/li&gt;
&lt;li&gt;C/C++&lt;/li&gt;
&lt;li&gt;C/C++ Extension Pack&lt;/li&gt;
&lt;li&gt;C/C++ Themes&lt;/li&gt;
&lt;li&gt;CMake&lt;/li&gt;
&lt;li&gt;CMake Tools&lt;/li&gt;
&lt;li&gt;Code Runner&lt;/li&gt;
&lt;li&gt;Doxygen Documentation Generator&lt;/li&gt;
&lt;li&gt;PlatformIO IDE&lt;/li&gt;
&lt;li&gt;XML Tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot;&amp;gt;
[1] The PlatformIO IDE plugin is easy to install. It takes a little bit longer to install. Once installed,
restart VS Code and make sure &lt;strong&gt;PIO Home&lt;/strong&gt; is displayed properly. As shown in the picture below
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/smartknob-build.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Flush Firmware&lt;/h2&gt;
&lt;p&gt;If everything is OK. You can see the following three icons in the toolbar under VS Code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../assets/blog/images/build-step.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;From left to right: Compile, Write, Serial Monitor.&lt;/p&gt;
&lt;h3&gt;Compile&lt;/h3&gt;
&lt;p&gt;Click &lt;strong&gt;Position 1&lt;/strong&gt; to start compile with following output：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; *  正在执行任务: C:\Users\piercebrands\.platformio\penv\Scripts\platformio.exe run

Processing view (board: esp32doit-devkit-v1; platform: espressif32@3.4; framework: arduino)
-------------------------------------------------------------------------------------------------------------------------------------------
Tool Manager: Installing platformio/toolchain-xtensa32 @ ~2.50200.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Tool Manager: toolchain-xtensa32@2.50200.97 has been installed!
Tool Manager: Installing platformio/framework-arduinoespressif32 @ ~3.10006.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Tool Manager: framework-arduinoespressif32@3.10006.210326 has been installed!
Tool Manager: Installing platformio/tool-esptoolpy @ ~1.30100.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Tool Manager: tool-esptoolpy@1.30100.210531 has been installed!
Library Manager: Installing askuric/Simple FOC @ 2.2.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: Simple FOC@2.2.0 has been installed!
Library Manager: Installing infineon/TLV493D-Magnetic-Sensor @ 1.0.3
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: TLV493D-Magnetic-Sensor@1.0.3 has been installed!
Library Manager: Installing bxparks/AceButton @ 1.9.1
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: AceButton@1.9.1 has been installed!
Library Manager: Installing bodmer/TFT_eSPI @ 2.4.25
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: TFT_eSPI@2.4.25 has been installed!
Library Manager: Installing fastled/FastLED @ 3.5.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: FastLED@3.5.0 has been installed!
Library Manager: Installing bogde/HX711 @ 0.7.5
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: HX711@0.7.5 has been installed!
Library Manager: Installing adafruit/Adafruit VEML7700 Library @ 1.1.1
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: Adafruit VEML7700 Library@1.1.1 has been installed!
Library Manager: Resolving dependencies...
Library Manager: Installing Adafruit BusIO
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Library Manager: Adafruit BusIO@1.13.2 has been installed!
Tool Manager: Installing platformio/tool-scons @ ~4.40400.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Tool Manager: tool-scons@4.40400.0 has been installed!
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32doit-devkit-v1.html
PLATFORM: Espressif 32 (3.4.0) &amp;gt; DOIT ESP32 DEVKIT V1
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (esp-prog) External (esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.10006.210326 (1.0.6)
 - tool-esptoolpy @ 1.30100.210531 (3.1.0)
 - toolchain-xtensa32 @ 2.50200.97 (5.2.0)
LDF: Library Dependency Finder -&amp;gt; https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 37 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Simple FOC @ 2.2.0
|   |-- Wire @ 1.0.1
|   |-- SPI @ 1.0
|-- TLV493D-Magnetic-Sensor @ 1.0.3
|   |-- Wire @ 1.0.1
|-- AceButton @ 1.9.1
|-- TFT_eSPI @ 2.4.25
|   |-- SPI @ 1.0
|   |-- SPIFFS @ 1.0
|   |   |-- FS @ 1.0
|   |-- FS @ 1.0
|-- FastLED @ 3.5.0
|   |-- SPI @ 1.0
|-- HX711 @ 0.7.5
|-- Adafruit VEML7700 Library @ 1.1.1
|   |-- Adafruit BusIO @ 1.13.2
|   |   |-- Wire @ 1.0.1
|   |   |-- SPI @ 1.0
|   |-- Wire @ 1.0.1
|   |-- SPI @ 1.0
Building in release mode
Compiling .pio\build\view\libcd9\FastLED\FastLED.cpp.o
Compiling .pio\build\view\libcd9\FastLED\bitswap.cpp.o
Compiling .pio\build\view\libcd9\FastLED\colorpalettes.cpp.o
Compiling .pio\build\view\libcd9\FastLED\colorutils.cpp.o
Compiling .pio\build\view\libcd9\FastLED\hsv2rgb.cpp.o
Compiling .pio\build\view\libcd9\FastLED\lib8tion.cpp.o
Compiling .pio\build\view\libcd9\FastLED\noise.cpp.o
Compiling .pio\build\view\libcd9\FastLED\platforms.cpp.o
Compiling .pio\build\view\libcd9\FastLED\platforms\esp\32\clockless_rmt_esp32.cpp.o
Compiling .pio\build\view\libcd9\FastLED\power_mgt.cpp.o
Compiling .pio\build\view\libcd9\FastLED\wiring.cpp.o
Compiling .pio\build\view\src\display_task.cpp.o
Compiling .pio\build\view\src\interface_task.cpp.o
Compiling .pio\build\view\src\main.cpp.o
Compiling .pio\build\view\src\motor_task.cpp.o
Compiling .pio\build\view\src\mt6701_sensor.cpp.o
Compiling .pio\build\view\src\tlv_sensor.cpp.o
Generating partitions .pio\build\view\partitions.bin
Compiling .pio\build\view\libb39\Wire\Wire.cpp.o
Compiling .pio\build\view\lib8f9\SPI\SPI.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\BLDCMotor.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\StepperMotor.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\common\base_classes\CurrentSense.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\common\base_classes\FOCMotor.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\common\base_classes\Sensor.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\common\foc_utils.cpp.o
Archiving .pio\build\view\lib8f9\libSPI.a
Indexing .pio\build\view\lib8f9\libSPI.a
Compiling .pio\build\view\lib44f\Simple FOC\common\lowpass_filter.cpp.o
.pio/libdeps/view/Simple FOC/src/common/foc_utils.cpp: In function &apos;float _sqrtApprox(float)&apos;:
.pio/libdeps/view/Simple FOC/src/common/foc_utils.cpp:68:21: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
   i = * ( long * ) &amp;amp;y;
                     ^
.pio/libdeps/view/Simple FOC/src/common/foc_utils.cpp:70:22: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
   y = * ( float * ) &amp;amp;i;
                      ^
src/motor_task.cpp: In member function &apos;void MotorTask::run()&apos;:
src/motor_task.cpp:280:37: warning: ISO C++ forbids converting a string constant to &apos;char*&apos; [-Wwrite-strings]
     command.add(&apos;M&apos;, &amp;amp;doMotor, &quot;foo&quot;);
                                     ^
src/motor_task.cpp:296:14: warning: unused variable &apos;last_debug&apos; [-Wunused-variable]
     uint32_t last_debug = 0;
              ^
src/mt6701_sensor.cpp: In member function &apos;virtual float MT6701Sensor::getSensorAngle()&apos;:
src/mt6701_sensor.cpp:94:15: warning: unused variable &apos;field_status&apos; [-Wunused-variable]
       uint8_t field_status = (spi_32 &amp;gt;&amp;gt; 6) &amp;amp; 0x3;
               ^
src/mt6701_sensor.cpp:95:15: warning: unused variable &apos;push_status&apos; [-Wunused-variable]
       uint8_t push_status = (spi_32 &amp;gt;&amp;gt; 8) &amp;amp; 0x1;
               ^
src/mt6701_sensor.cpp:96:15: warning: unused variable &apos;loss_status&apos; [-Wunused-variable]
       uint8_t loss_status = (spi_32 &amp;gt;&amp;gt; 9) &amp;amp; 0x1;
               ^
.pio/libdeps/view/FastLED/src/platforms/esp/32/clockless_rmt_esp32.cpp: In static member function &apos;static void ESP32RMTController::init(gpio_num_t)&apos;:
.pio/libdeps/view/FastLED/src/platforms/esp/32/clockless_rmt_esp32.cpp:111:15: warning: variable &apos;espErr&apos; set but not used [-Wunused-but-set-variable]
     esp_err_t espErr = ESP_OK;
               ^
.pio/libdeps/view/FastLED/src/platforms/esp/32/clockless_rmt_esp32.cpp: In member function &apos;void ESP32RMTController::startOnChannel(int)&apos;:
.pio/libdeps/view/FastLED/src/platforms/esp/32/clockless_rmt_esp32.cpp:239:15: warning: variable &apos;espErr&apos; set but not used [-Wunused-but-set-variable]
     esp_err_t espErr = ESP_OK;
               ^
Compiling .pio\build\view\lib44f\Simple FOC\common\pid.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\common\time_utils.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\communication\Commander.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\communication\StepDirListener.cpp.o
Archiving .pio\build\view\libb39\libWire.a
In file included from .pio/libdeps/view/FastLED/src/FastLED.h:67:0,
                 from src/interface_task.cpp:4:
.pio/libdeps/view/FastLED/src/fastspi.h:145:23: note: #pragma message: No hardware SPI pins defined.  All SPI access will default to bitbanged output
 #      pragma message &quot;No hardware SPI pins defined.  All SPI access will default to bitbanged output&quot;
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\InlineCurrentSense.cpp.o
                       ^
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\LowsideCurrentSense.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\atmega_mcu.cpp.o
Indexing .pio\build\view\libb39\libWire.a
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\due_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\esp32_adc_driver.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\esp32_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\generic_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\samd21_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\samd_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\stm32_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\stm32g4_hal.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\stm32g4_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\current_sense\hardware_specific\teensy_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\BLDCDriver3PWM.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\BLDCDriver6PWM.cpp.o
src/display_task.cpp: In member function &apos;void DisplayTask::run()&apos;:
src/display_task.cpp:106:13: warning: unused variable &apos;pointer_center_x&apos; [-Wunused-variable]
     int32_t pointer_center_x = TFT_WIDTH / 2;
             ^
src/display_task.cpp:107:13: warning: unused variable &apos;pointer_center_y&apos; [-Wunused-variable]
     int32_t pointer_center_y = TFT_HEIGHT / 2;
             ^
src/display_task.cpp:108:13: warning: unused variable &apos;pointer_length_short&apos; [-Wunused-variable]
     int32_t pointer_length_short = 10;
             ^
src/display_task.cpp:109:13: warning: unused variable &apos;pointer_length_long&apos; [-Wunused-variable]
     int32_t pointer_length_long = TFT_WIDTH / 2 - 5;
             ^
Compiling .pio\build\view\lib44f\Simple FOC\drivers\StepperDriver2PWM.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\StepperDriver4PWM.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\atmega2560_mcu.cpp.o
src/display_task.cpp: At global scope:
src/display_task.cpp:22:13: warning: &apos;void HSV_to_RGB(float, float, float, uint8_t*, uint8_t*, uint8_t*)&apos; defined but not used [-Wunused-function]
 static void HSV_to_RGB(float h, float s, float v, uint8_t *r, uint8_t *g, uint8_t *b)
             ^
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\atmega328_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\atmega32u4_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\due_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\esp32_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\esp8266_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\generic_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\portenta_h7_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\rp2040_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\samd21_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\samd51_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\samd_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\stm32_mcu.cpp.o
.pio/libdeps/view/Simple FOC/src/current_sense/hardware_specific/esp32_mcu.cpp: In function &apos;float _readADCVoltageLowSide(int)&apos;:
.pio/libdeps/view/Simple FOC/src/current_sense/hardware_specific/esp32_mcu.cpp:60:18: warning: &apos;raw_adc&apos; may be used uninitialized in this
function [-Wmaybe-uninitialized]
   return raw_adc * _ADC_CONV;
                  ^
Compiling .pio\build\view\lib44f\Simple FOC\drivers\hardware_specific\teensy_mcu.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\Encoder.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\HallSensor.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\MagneticSensorAnalog.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\MagneticSensorI2C.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\MagneticSensorPWM.cpp.o
Compiling .pio\build\view\lib44f\Simple FOC\sensors\MagneticSensorSPI.cpp.o
Compiling .pio\build\view\lib1b5\tlv\Tlv493d.cpp.o
Compiling .pio\build\view\lib1b5\tlv\util\BusInterface.cpp.o
Compiling .pio\build\view\lib1b5\tlv\util\RegMask.cpp.o
Compiling .pio\build\view\lib02a\AceButton\ace_button\AceButton.cpp.o
Compiling .pio\build\view\lib02a\AceButton\ace_button\ButtonConfig.cpp.o
Compiling .pio\build\view\lib02a\AceButton\ace_button\EncodedButtonConfig.cpp.o
Compiling .pio\build\view\lib02a\AceButton\ace_button\LadderButtonConfig.cpp.o
Compiling .pio\build\view\lib02a\AceButton\ace_button\testing\EventTracker.cpp.o
Compiling .pio\build\view\lib65c\FS\FS.cpp.o
Compiling .pio\build\view\lib65c\FS\vfs_api.cpp.o
Compiling .pio\build\view\lib725\SPIFFS\SPIFFS.cpp.o
Compiling .pio\build\view\lib067\TFT_eSPI\TFT_eSPI.cpp.o
Compiling .pio\build\view\libbf2\HX711\HX711.cpp.o
Compiling .pio\build\view\liba5e\Adafruit BusIO\Adafruit_BusIO_Register.cpp.o
Compiling .pio\build\view\liba5e\Adafruit BusIO\Adafruit_I2CDevice.cpp.o
Compiling .pio\build\view\liba5e\Adafruit BusIO\Adafruit_SPIDevice.cpp.o
Compiling .pio\build\view\lib905\Adafruit VEML7700 Library\Adafruit_VEML7700.cpp.o
Archiving .pio\build\view\libFrameworkArduinoVariant.a
Indexing .pio\build\view\libFrameworkArduinoVariant.a
Compiling .pio\build\view\FrameworkArduino\Esp.cpp.o
Compiling .pio\build\view\FrameworkArduino\FunctionalInterrupt.cpp.o
Compiling .pio\build\view\FrameworkArduino\HardwareSerial.cpp.o
Compiling .pio\build\view\FrameworkArduino\IPAddress.cpp.o
Compiling .pio\build\view\FrameworkArduino\IPv6Address.cpp.o
Compiling .pio\build\view\FrameworkArduino\MD5Builder.cpp.o
Archiving .pio\build\view\lib44f\libSimple FOC.a
Compiling .pio\build\view\FrameworkArduino\Print.cpp.o
Archiving .pio\build\view\lib1b5\libtlv.a
Compiling .pio\build\view\FrameworkArduino\Stream.cpp.o
Indexing .pio\build\view\lib1b5\libtlv.a
Compiling .pio\build\view\FrameworkArduino\StreamString.cpp.o
Compiling .pio\build\view\FrameworkArduino\WMath.cpp.o
Compiling .pio\build\view\FrameworkArduino\WString.cpp.o
Compiling .pio\build\view\FrameworkArduino\base64.cpp.o
Indexing .pio\build\view\lib44f\libSimple FOC.a
Compiling .pio\build\view\FrameworkArduino\cbuf.cpp.o
Archiving .pio\build\view\lib02a\libAceButton.a
Indexing .pio\build\view\lib02a\libAceButton.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-adc.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-bt.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-cpu.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-dac.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-gpio.c.o
Archiving .pio\build\view\libbf2\libHX711.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-i2c.c.o
Indexing .pio\build\view\libbf2\libHX711.a
Archiving .pio\build\view\lib725\libSPIFFS.a
Indexing .pio\build\view\lib725\libSPIFFS.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-ledc.c.o
Archiving .pio\build\view\liba5e\libAdafruit BusIO.a
Archiving .pio\build\view\lib65c\libFS.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-log.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-matrix.c.o
Indexing .pio\build\view\lib65c\libFS.a
Indexing .pio\build\view\liba5e\libAdafruit BusIO.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-misc.c.o
Archiving .pio\build\view\lib905\libAdafruit VEML7700 Library.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-psram.c.o
Indexing .pio\build\view\lib905\libAdafruit VEML7700 Library.a
Compiling .pio\build\view\FrameworkArduino\esp32-hal-rmt.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-sigmadelta.c.o
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp: In member function &apos;virtual int16_t TFT_eSPI::drawChar(uint16_t, int32_t, int32_t, uint8_t)&apos;:
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4220:12: warning: unused variable &apos;flash_address&apos; [-Wunused-variable]
   uint32_t flash_address = 0;
            ^
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4249:11: warning: unused variable &apos;w&apos; [-Wunused-variable]
   int32_t w = width;
           ^
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4250:11: warning: unused variable &apos;pX&apos; [-Wunused-variable]
   int32_t pX      = 0;
           ^
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4251:11: warning: unused variable &apos;pY&apos; [-Wunused-variable]
   int32_t pY      = y;
           ^
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4252:11: warning: unused variable &apos;line&apos; [-Wunused-variable]
   uint8_t line = 0;
           ^
.pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:4253:8: warning: unused variable &apos;clip&apos; [-Wunused-variable]
   bool clip = xd &amp;lt; _vpX || xd + width  * textsize &amp;gt;= _vpW || yd &amp;lt; _vpY || yd + height * textsize &amp;gt;= _vpH;
        ^
In file included from .pio/libdeps/view/TFT_eSPI/TFT_eSPI.cpp:5078:0:
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp: In member function &apos;virtual int16_t TFT_eSprite::drawChar(uint16_t, int32_t, int32_t, uint8_t)&apos;:
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2181:12: warning: unused variable &apos;flash_address&apos; [-Wunused-variable]
   uint32_t flash_address = 0;
            ^
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2210:11: warning: unused variable &apos;w&apos; [-Wunused-variable]
   int32_t w = width;
           ^
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2211:11: warning: unused variable &apos;pX&apos; [-Wunused-variable]
   int32_t pX      = 0;
           ^
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2212:11: warning: unused variable &apos;pY&apos; [-Wunused-variable]
   int32_t pY      = y;
           ^
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2213:11: warning: unused variable &apos;line&apos; [-Wunused-variable]
   uint8_t line = 0;
           ^
.pio/libdeps/view/TFT_eSPI/Extensions/Sprite.cpp:2214:8: warning: unused variable &apos;clip&apos; [-Wunused-variable]
   bool clip = xd &amp;lt; _vpX || xd + width  * textsize &amp;gt;= _vpW || yd &amp;lt; _vpY || yd + height * textsize &amp;gt;= _vpH;
        ^
Compiling .pio\build\view\FrameworkArduino\esp32-hal-spi.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-time.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-timer.c.o
C:/Users/piercebrands/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-spi.c: In function &apos;spiTransferBytesNL&apos;:
C:/Users/piercebrands/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-spi.c:922:39: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
                 uint8_t * last_out8 = &amp;amp;result[c_longs-1];
                                       ^
C:/Users/piercebrands/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-spi.c:923:40: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
                 uint8_t * last_data8 = &amp;amp;last_data;
                                        ^
Compiling .pio\build\view\FrameworkArduino\esp32-hal-touch.c.o
Compiling .pio\build\view\FrameworkArduino\esp32-hal-uart.c.o
Compiling .pio\build\view\FrameworkArduino\libb64\cdecode.c.o
Compiling .pio\build\view\FrameworkArduino\libb64\cencode.c.o
Compiling .pio\build\view\FrameworkArduino\main.cpp.o
Compiling .pio\build\view\FrameworkArduino\stdlib_noniso.c.o
Compiling .pio\build\view\FrameworkArduino\wiring_pulse.c.o
Compiling .pio\build\view\FrameworkArduino\wiring_shift.c.o
Archiving .pio\build\view\libFrameworkArduino.a
Indexing .pio\build\view\libFrameworkArduino.a
Archiving .pio\build\view\lib067\libTFT_eSPI.a
Indexing .pio\build\view\lib067\libTFT_eSPI.a
Linking .pio\build\view\firmware.elf
Retrieving maximum program size .pio\build\view\firmware.elf
Checking size .pio\build\view\firmware.elf
Advanced Memory Usage is available via &quot;PlatformIO Home &amp;gt; Project Inspect&quot;
RAM:   [=         ]   5.4% (used 17648 bytes from 327680 bytes)
Flash: [===       ]  27.3% (used 357566 bytes from 1310720 bytes)
Building .pio\build\view\firmware.bin
esptool.py v3.1
Merged 1 ELF section
====================================================== [SUCCESS] Took 412.84 seconds ======================================================
Environment    Status    Duration
-------------  --------  ------------
view           SUCCESS   00:06:52.838
======================================================= 1 succeeded in 00:06:52.838 ======================================================= *  终端将被任务重用，按任意键关闭。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;{/* &amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
The first compilation will take a bit longer as there are some plugins to download. &lt;strong&gt;If it keeps stopping, try switching
networks or closing the current shell and clicking the compile button here to re-download.&lt;/strong&gt;
&amp;lt;/Aside&amp;gt; */}&lt;/p&gt;
&lt;p&gt;If you see output similar to the above, the compilation is complete.&lt;/p&gt;
&lt;h3&gt;Write&lt;/h3&gt;
&lt;p&gt;After the compilation is complete, click the Write button on &lt;strong&gt;Position 2&lt;/strong&gt; and the write log will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; *  正在执行任务: C:\Users\piercebrands\.platformio\penv\Scripts\platformio.exe run --target upload

Processing view (board: esp32doit-devkit-v1; platform: espressif32@3.4; framework: arduino)
-------------------------------------------------------------------------------------------------------------------------------------------
Tool Manager: Installing platformio/tool-mkspiffs @ ~2.230.0
Downloading  [####################################]  100%
Unpacking  [####################################]  100%
Tool Manager: tool-mkspiffs@2.230.0 has been installed!
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32doit-devkit-v1.html
PLATFORM: Espressif 32 (3.4.0) &amp;gt; DOIT ESP32 DEVKIT V1
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (esp-prog) External (esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
 - framework-arduinoespressif32 @ 3.10006.210326 (1.0.6)
 - tool-esptoolpy @ 1.30100.210531 (3.1.0)
 - tool-mkspiffs @ 2.230.0 (2.30)
 - toolchain-xtensa32 @ 2.50200.97 (5.2.0)
LDF: Library Dependency Finder -&amp;gt; https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 37 compatible libraries
Scanning dependencies...
Dependency Graph
|-- Simple FOC @ 2.2.0
|   |-- Wire @ 1.0.1
|   |-- SPI @ 1.0
|-- TLV493D-Magnetic-Sensor @ 1.0.3
|   |-- Wire @ 1.0.1
|-- AceButton @ 1.9.1
|-- TFT_eSPI @ 2.4.25
|   |-- SPI @ 1.0
|   |-- SPIFFS @ 1.0
|   |   |-- FS @ 1.0
|   |-- FS @ 1.0
|-- FastLED @ 3.5.0
|   |-- SPI @ 1.0
|-- HX711 @ 0.7.5
|-- Adafruit VEML7700 Library @ 1.1.1
|   |-- Adafruit BusIO @ 1.13.2
|   |   |-- Wire @ 1.0.1
|   |   |-- SPI @ 1.0
|   |-- Wire @ 1.0.1
|   |-- SPI @ 1.0
Building in release mode
Retrieving maximum program size .pio\build\view\firmware.elf
Checking size .pio\build\view\firmware.elf
Advanced Memory Usage is available via &quot;PlatformIO Home &amp;gt; Project Inspect&quot;
RAM:   [=         ]   5.4% (used 17648 bytes from 327680 bytes)
Flash: [===       ]  27.3% (used 357566 bytes from 1310720 bytes)
Configuring upload protocol...
AVAILABLE: esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Auto-detected: COM3
Uploading .pio\build\view\firmware.bin
esptool.py v3.1
Serial port COM3
Connecting......
Chip is ESP32-PICO-V3-02 (revision 3)
Features: WiFi, BT, Dual Core, 240MHz, Embedded Flash, Embedded PSRAM, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: 98:cd:ac:e8:af:ec
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 8MB
Flash will be erased from 0x00001000 to 0x00005fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x0000e000 to 0x0000ffff...
Flash will be erased from 0x00010000 to 0x00067fff...
Flash params set to 0x0230
Compressed 17104 bytes to 11191...
Writing at 0x00001000... (100 %)
Wrote 17104 bytes (11191 compressed) at 0x00001000 in 0.6 seconds (effective 229.4 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 128...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (128 compressed) at 0x00008000 in 0.1 seconds (effective 364.0 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...
Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 541.4 kbit/s)...
Hash of data verified.
Compressed 357680 bytes to 190508...
Writing at 0x00010000... (8 %)
Writing at 0x00018fcf... (16 %)
Writing at 0x0002ae74... (25 %)
Writing at 0x0003201b... (33 %)
Writing at 0x00037658... (41 %)
Writing at 0x0003ce83... (50 %)
Writing at 0x0004347a... (58 %)
Writing at 0x0004c06f... (66 %)
Writing at 0x000519bf... (75 %)
Writing at 0x00057a46... (83 %)
Writing at 0x0005d864... (91 %)
Writing at 0x00063652... (100 %)
Wrote 357680 bytes (190508 compressed) at 0x00010000 in 4.4 seconds (effective 651.0 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
====================================================== [SUCCESS] Took 19.27 seconds ======================================================

Environment    Status    Duration
-------------  --------  ------------
view           SUCCESS   00:00:19.271
======================================================= 1 succeeded in 00:00:19.271 =======================================================
*  终端将被任务重用，按任意键关闭。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Serial Monitor&lt;/h3&gt;
&lt;p&gt;Waiting the write task is complete, press the serial button on the &lt;strong&gt;Position 3&lt;/strong&gt; and the output is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; *  正在执行任务: C:\Users\piercebrands\.platformio\penv\Scripts\platformio.exe device monitor

--- Terminal on COM3 | 115200 8-N-1
--- Available filters and text transformations: colorize, debug, default, direct,
esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:1044
load:0x40078000,len:10124
load:0x40080400,len:5828
entry 0x400806a8
Sprite created!
millilux: 0.00
HX711 reading: 669031
millilux: 0.00
HX711 reading: 668815
Press Y to run calibration          &amp;lt;========= // Look this
millilux: 0.00
HX711 reading: 668869
millilux: 0.00
HX711 reading: 668639
millilux: 0.00
HX711 reading: 668634
4.43
Got new config
millilux: 0.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a word &lt;strong&gt;Press Y to run calibration&lt;/strong&gt;, which can be used to automatically debug a set value suitable for the current device.
After pressing the capital Y in the serial window, the knob will be rotated automatically, and after a while, the following result
will be output in the serial port:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RESULTS:
    zero electric angle: 4.43 // The values on this side may be different for each device.
    direction: CW
    pole pairs: 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;strong&gt;motor_task.cpp&lt;/strong&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float zero_electric_offset = 4.43; // Replace the value with the value of zero electric angle output above.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, write the firmware again. It should run successfully.&lt;/p&gt;
</content:encoded></item><item><title>如何在 macOS 上从源码构建 manimgl</title><link>https://obiscr.com/zh-cn/blog/build-manimgl-from-source</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/build-manimgl-from-source</guid><description>本文介绍了如何在 macOS 上从源码构建 manimgl。</description><content:encoded>&lt;p&gt;import LoopingVideo from &apos;~/components/looping-video.astro&apos;
import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;Manim 是一个精确的程序化动画引擎，用于创建解释性数学视频。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
Manim 有两个版本，一个是由 &lt;a href=&quot;https://3blue1brown.com&quot;&gt;3blue1brown&lt;/a&gt; 维护的个人版本，另一个是由 &lt;a href=&quot;https://www.manim.community/&quot;&gt;ManimCommunity&lt;/a&gt; 维护的社区版本。
本文将演示由 &lt;a href=&quot;https://3blue1brown.com&quot;&gt;3blue1brown&lt;/a&gt; 维护的 macOS 上的个人版本。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;Download source code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd ~/projects
git clone https://github.com/3b1b/manim.git
cd manim
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建环境&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;conda create -n manim-build-from-source python=3.10
conda activate manim-build-from-source
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装库&lt;/h2&gt;
&lt;h3&gt;mapbox-earcut&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install mapbox-earcut
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ffmpeg&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install ffmpeg
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mactex&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;brew install mactex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tips&quot;&amp;gt;
This may take a little longer.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h4&gt;验证 mactex 安装&lt;/h4&gt;
&lt;p&gt;Type &lt;code&gt;latex --version&lt;/code&gt; in the terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;latex --version

# Output
pdfTeX 3.141592653-2.6-1.40.26 (TeX Live 2024)
kpathsea version 6.4.0
Copyright 2024 Han The Thanh (pdfTeX) et al.
There is NO warranty.  Redistribution of this software is
covered by the terms of both the pdfTeX copyright and
the Lesser GNU General Public License.
For more information about these matters, see the file
named COPYING and the pdfTeX source.
Primary author of pdfTeX: Han The Thanh (pdfTeX) et al.
Compiled with libpng 1.6.43; using libpng 1.6.43
Compiled with zlib 1.3.1; using zlib 1.3.1
Compiled with xpdf version 4.04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出如上所示，那么恭喜你，mactex 已成功安装。你可以继续 &lt;a href=&quot;#build-from-source&quot;&gt;下一步&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;否则，请继续。我们需要修复这个问题。
我不确定为什么，但有时，当 brew install mactex 完成后，它只是下载一个包到你的磁盘。你仍然需要手动安装它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew info mactex

# Output
❯ brew info mactex
==&amp;gt; mactex: 2024.0312
https://www.tug.org/mactex/
Installed
/opt/homebrew/Caskroom/mactex/2024.0312 (5.6GB)
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/m/mactex.rb
==&amp;gt; Name
MacTeX
==&amp;gt; Description
Full TeX Live distribution with GUI applications
==&amp;gt; Dependencies
ghostscript
==&amp;gt; Artifacts
mactex-20240312.pkg (Pkg)
==&amp;gt; Caveats
You must restart your terminal window for the installation of MacTeX CLI
tools to take effect.

Alternatively, Bash and Zsh users can run the command:

  eval &quot;$(/usr/libexec/path_helper)&quot;

==&amp;gt; Analytics
install: 3,166 (30 days), 7,794 (90 days), 30,329 (365 days)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制高亮的路径并在终端中打开它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;open /opt/homebrew/Caskroom/mactex/2024.031
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到一个名为 &lt;code&gt;mactex-&amp;lt;date&amp;gt;.pkg&lt;/code&gt; 的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tree /opt/homebrew/Caskroom/mactex/2024.0312
/opt/homebrew/Caskroom/mactex/2024.0312
└── mactex-20240312.pkg

1 directory, 1 file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开这个文件并手动安装。&lt;/p&gt;
&lt;p&gt;安装完成后，重新 &lt;a href=&quot;#verify-mactex-installation&quot;&gt;验证 mactex 安装&lt;/a&gt;，现在 &lt;code&gt;mactex&lt;/code&gt; 应该可以正常工作。&lt;/p&gt;
&lt;h2&gt;从源码构建&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pip install -e ./manim
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证 manimgl 安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;which manimgl # output: /opt/anaconda3/envs/manim-build-from-source/bin/manimgl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;tip&quot; title=&quot;Tips&quot;&amp;gt;
这可能因系统而异，但可以通过虚拟环境的名称进行验证，只要 &lt;code&gt;manimgl&lt;/code&gt; 来自 &lt;code&gt;manim-build-from-source/bin/manimgl&lt;/code&gt; 目录，那么就没有问题。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;使用 manimgl&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd ~/projects/manim
manimgl example_scenes.py TexTransformExample
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你应该能够看到下面的动画。&lt;/p&gt;
&lt;p&gt;&amp;lt;LoopingVideo sources={[{ src: &apos;/public/blog/videos/TexTransformExample.mp4&apos;, type: &apos;video/mp4&apos; }]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;恭喜你，你已经完成了所有步骤。&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;p&gt;如果遇到安装和使用问题，可以在 &lt;a href=&quot;https://github.com/3b1b/manim/issues/new&quot;&gt;这里&lt;/a&gt; 创建一个 Issue。&lt;/p&gt;
&lt;p&gt;查看 &lt;a href=&quot;https://3b1b.github.io/manim/getting_started/example_scenes.html&quot;&gt;示例场景&lt;/a&gt; 以查看库的语法、动画类型和对象类型示例。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://github.com/3b1b/videos&quot;&gt;3b1b/videos&lt;/a&gt; 仓库中，你可以看到所有 3blue1brown 视频的代码。&lt;/p&gt;
</content:encoded></item><item><title>基于 VSCode 构建自己的远程开发环境</title><link>https://obiscr.com/zh-cn/blog/building-your-own-remote-dev-env-based-on-vscode</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/building-your-own-remote-dev-env-based-on-vscode</guid><description>本文介绍了如何基于vscode构建自己的远程开发环境。</description><content:encoded>&lt;p&gt;import { Aside, Badge, FileTree, LinkCard } from &apos;@astrojs/starlight/components&apos;;
import ReadMore from &apos;~/components/read-more.astro&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/microsoft/vscode.git&quot;
title=&quot;VSCode Source Code Repository&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;平台要求:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;System: &lt;a href=&quot;https://ubuntu.com/download/desktop&quot;&gt;Ubuntu 24.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/zh-cn/download&quot;&gt;Node.js&lt;/a&gt;: v22.21.1&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fly.io/docs/flyctl/install/&quot;&gt;Fly.io CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;Aside title=&quot;注意&quot;&amp;gt;
上述需要的平台/账号请自行注册，这里不再赘述。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;构建 VSCode&lt;/h2&gt;
&lt;h3&gt;下载 VSCode 源码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/microsoft/vscode.git
cd vscode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们将基于一个历史版本 &amp;lt;Badge text=&apos;v1.99.3&apos;/&amp;gt; 来构建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b my-vscode-1.99.3 1.99.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;ReadMore&amp;gt;参考 &lt;a href=&quot;https://github.com/microsoft/vscode/wiki/How-to-Contribute&quot;&gt;How to build and run from source&lt;/a&gt;&amp;lt;/ReadMore&amp;gt;&lt;/p&gt;
&lt;h3&gt;编译 VSCode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cd vscode
npm install
npm run compile
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构建 Docker 镜像&lt;/h2&gt;
&lt;h3&gt;创建构建脚本&lt;/h3&gt;
&lt;p&gt;在项目根目录新建 &lt;code&gt;Dockerfile&lt;/code&gt; 文件，并在scripts目录下新建 &lt;code&gt;build.sh&lt;/code&gt; 和 &lt;code&gt;entrypoint.sh&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;vscode
&lt;ul&gt;
&lt;li&gt;scripts/
&lt;ul&gt;
&lt;li&gt;build.sh&lt;/li&gt;
&lt;li&gt;entrypoint.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dockerfile
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR=$(cd &quot;$(dirname &quot;$0&quot;)/..&quot; &amp;amp;&amp;amp; pwd)
cd &quot;$ROOT_DIR&quot;

IMAGE_NAME=&quot;${IMAGE_NAME:-vscode-web}&quot;
IMAGE_TAG=&quot;${IMAGE_TAG:-local}&quot;
DOCKERFILE=&quot;${DOCKERFILE:-Dockerfile}&quot;
WEB_DIST_DIR=&quot;${WEB_DIST_DIR:-vscode-reh-web-linux-x64}&quot;

echo &quot;[ENV] IMAGE_NAME=${IMAGE_NAME} IMAGE_TAG=${IMAGE_TAG} DOCKERFILE=${DOCKERFILE}&quot;
echo &quot;[ENV] WEB_DIST_DIR=${WEB_DIST_DIR}&quot;

echo &quot;[STEP] Build VS Code web dist -&amp;gt; ${WEB_DIST_DIR}&quot;
rm -rf &quot;${WEB_DIST_DIR}&quot;
npm run gulp &quot;${WEB_DIST_DIR}&quot;
cp -r ../&quot;${WEB_DIST_DIR}&quot; ./

echo &quot;[STEP] Docker build ${IMAGE_NAME}:${IMAGE_TAG}&quot;
docker build \
  --build-arg WEB_DIST_DIR=&quot;${WEB_DIST_DIR}&quot; \
  -t &quot;${IMAGE_NAME}:${IMAGE_TAG}&quot; \
  -f &quot;${DOCKERFILE}&quot; \
  &quot;${ROOT_DIR}&quot;

echo &quot;[DONE] Built image: ${IMAGE_NAME}:${IMAGE_TAG}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail

log() {
  echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] $*&quot;
}

APP_HOME=&quot;${APP_HOME:-/opt/vscode-web}&quot;
HOST=&quot;0.0.0.0&quot;
LISTEN_PORT=&quot;${IDE_PORT:-8080}&quot;
CONNECTION_TOKEN=&quot;${CONNECTION_TOKEN:-rviFq8oBBOIp92fGXnSlWygbjNpGU2FelEeMVQp6CTRiuux0BxGKU01yCCmICBbY}&quot;

ARGS=(
  --host &quot;${HOST}&quot;
  --port &quot;${LISTEN_PORT}&quot;
  --connection-token &quot;${CONNECTION_TOKEN}&quot;
)

log &quot;[INFO] Starting VS Code Web on ${HOST}:${LISTEN_PORT}?tkn=${CONNECTION_TOKEN}&quot;
log &quot;[INFO] APP_HOME=${APP_HOME}&quot;

SERVER_BIN=&quot;${SERVER_BIN:-${APP_HOME}/bin/code-server-oss}&quot;

exec &quot;${SERVER_BIN}&quot; &quot;${ARGS[@]}&quot; &quot;$@&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;FROM ubuntu:24.04

ARG WEB_DIST_DIR=vscode-reh-web-linux-x64

ENV DEBIAN_FRONTEND=noninteractive \
    APP_HOME=/opt/vscode-web \
    IDE_PORT=8080 \
    PNPM_STORE_DIR=/home/vscode/workspace/.pnpm-store

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    bash \
    dumb-init \
    git \
    unzip \
    zip \
    wget \
    net-tools \
    lrzsz \
    gnupg \
 &amp;amp;&amp;amp; curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
 &amp;amp;&amp;amp; apt-get install -y --no-install-recommends nodejs \
 &amp;amp;&amp;amp; npm install -g pnpm@9 \
 &amp;amp;&amp;amp; npm cache clean --force \
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

RUN groupadd -r vscode &amp;amp;&amp;amp; useradd -m -r -g vscode -s /bin/bash vscode

RUN mkdir -p &quot;$APP_HOME&quot; /home/vscode/workspace &quot;$PNPM_STORE_DIR&quot; \
 &amp;amp;&amp;amp; chown -R vscode:vscode &quot;$APP_HOME&quot; /home/vscode

COPY ${WEB_DIST_DIR}/ &quot;$APP_HOME&quot;/

COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod 0755 /usr/local/bin/entrypoint.sh \
 &amp;amp;&amp;amp; chown -R vscode:vscode &quot;$APP_HOME&quot;

USER vscode

RUN pnpm config set store-dir &quot;$PNPM_STORE_DIR&quot; --global \
 &amp;amp;&amp;amp; git config --global user.name &quot;vscode&quot; \
 &amp;amp;&amp;amp; git config --global user.email &quot;vscode@local.com&quot;

WORKDIR /home/vscode/workspace

EXPOSE 8080

ENTRYPOINT [&quot;/usr/bin/dumb-init&quot;, &quot;--&quot;]
CMD [&quot;/usr/local/bin/entrypoint.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后开始构建镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd vscode
bash scripts/build.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建完成之后，可以使用 &lt;code&gt;docker images&lt;/code&gt; 来查看构建好的镜像。&lt;/p&gt;
&lt;h3&gt;推送镜像到 Dockerhub&lt;/h3&gt;
&lt;p&gt;注册一个 &lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt; 账号，并安装 &lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside title=&quot;注意&quot;&amp;gt;
把 &lt;code&gt;{username}&lt;/code&gt; 替换为你自己的用户名
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker login
docker tag vscode-web:local {username}/vscode-web:latest
docker push {username}/vscode-web:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;部署到 Fly.io&lt;/h2&gt;
&lt;p&gt;&amp;lt;Aside title=&quot;注意&quot;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 &lt;code&gt;{username}&lt;/code&gt; 替换为你自己的用户名。&lt;/li&gt;
&lt;li&gt;下方的 &lt;code&gt;fly.toml&lt;/code&gt; 里面的 &lt;code&gt;region&lt;/code&gt;。尽可能选择离自己最近的。所有可用的 &lt;code&gt;region&lt;/code&gt; 见 &lt;a href=&quot;https://fly.io/docs/reference/regions/&quot;&gt;Fly.io Regions&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;下方的 &lt;code&gt;fly.toml&lt;/code&gt; 里面的 &lt;code&gt;app&lt;/code&gt;，必须是要唯一的，如果重复，会创建失败。可以添加一些随机的字符。只要保证不重复就可以。
&amp;lt;/Aside&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;登陆 Fly.io&lt;/h3&gt;
&lt;p&gt;注册一个 &lt;a href=&quot;https://fly.io/&quot;&gt;Fly.io&lt;/a&gt; 账号，并安装 &lt;a href=&quot;https://fly.io/docs/flyctl/install/&quot;&gt;Fly.io CLI&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fly auth login
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 Token&lt;/h3&gt;
&lt;p&gt;把 &lt;code&gt;~/.fly/config.yml&lt;/code&gt; 文件中的 &lt;code&gt;access_token&lt;/code&gt; 配置到 &lt;code&gt;FLY_API_TOKEN&lt;/code&gt; 环境变量中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export FLY_API_TOKEN=$(cat ~/.fly/config.yml | grep access_token | awk -F &apos;: &apos; &apos;{print $2}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建 App&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fly apps create {username}-vscode-web-ide
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建 Volume&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fly volumes create vscode_workspace --size 3 --region sin --app {username}-vscode-web-ide
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建 fly.toml 文件&lt;/h3&gt;
&lt;p&gt;在项目根目录新建 &lt;code&gt;fly.toml&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app = &apos;{username}-vscode-web-ide&apos;
primary_region = &apos;sjc&apos;

[build]
  image = &apos;{username}/vscode-web:latest&apos;

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = &apos;suspend&apos;
  auto_start_machines = true
  min_machines_running = 0
  processes = [&apos;app&apos;]

[[vm]]
  memory = &apos;4gb&apos;
  cpu_kind = &apos;shared&apos;
  cpus = 8

[[mounts]]
  source = &quot;vscode_workspace&quot;
  destination = &quot;/home/vscode/workspace&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以开始部署了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fly deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;访问 VSCode Web&lt;/h2&gt;
&lt;p&gt;如果一切正常，那么现在可以正常访问 VSCode Web 了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://{username}-vscode-web-ide.fly.dev?tkn=rviFq8oBBOIp92fGXnSlWygbjNpGU2FelEeMVQp6CTRiuux0BxGKU01yCCmICBbY
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>如何向开发者反馈</title><link>https://obiscr.com/zh-cn/blog/how-to-report-bugs</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/how-to-report-bugs</guid><description>当插件遇到问题时，也许以下是解决问题的更快更好的方法。</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;如何向开发者反馈&lt;/h2&gt;
&lt;p&gt;不可避免地，我们在使用插件时会遇到一些问题。为了快速解决问题，我们需要一种快速有效的方式与开发者沟通。减少可能的时间损失和无用的重复操作，并尽快解决问题。&lt;/p&gt;
&lt;h2&gt;出现错误&lt;/h2&gt;
&lt;p&gt;Here, for example, we use the &lt;a href=&quot;https://plugins.jetbrains.com/plugin/20603-chatgpt--easycode&quot;&gt;ChatGPT - EasyCode&lt;/a&gt; plugin to simulate an error.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/bug-report.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里的标记是&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plgugin version info&lt;/li&gt;
&lt;li&gt;Author&apos;s e-mail or website&lt;/li&gt;
&lt;li&gt;Error message&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;尝试联系作者&lt;/h2&gt;
&lt;p&gt;如 &lt;code&gt;marker 2&lt;/code&gt; 所示，你可以找到插件作者的定制 &lt;strong&gt;网站或电子邮件&lt;/strong&gt;。首先，你可以尝试询问帮助。也可以在论坛（如果有）中寻求帮助，也许你不是唯一一个遇到这个问题的人。也可以在插件的评论页留下消息。&lt;/p&gt;
&lt;p&gt;Anyway, the first thing to do is to make sure that there is at least one way to contact the author.&lt;/p&gt;
&lt;h2&gt;提供必要的信息&lt;/h2&gt;
&lt;p&gt;当出现错误时，开发者最需要知道的信息是以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;错误日志&lt;/strong&gt; (在图片中的 &lt;code&gt;marker 3&lt;/code&gt;): 这里的信息很重要。在大多数情况下，我们可以根据这些信息快速定位问题。复制这里的一切并将其作为附件发送给作者。这是非常必要的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件版本&lt;/strong&gt; (在图片中的 &lt;code&gt;marker 1&lt;/code&gt;): 当插件出现错误时，我们通常可以看到插件版本，这也是一个重要的信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE 版本&lt;/strong&gt;: 有时 SDK 兼容性或其他原因导致某些错误仅在特定版本的 IDE 上出现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idea.log&lt;/strong&gt;: 这是一个更完整的日志文件，用于记录 IDE 和插件的一些信息。在某些情况下，插件作者可能也需要你提供这个文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如何找到 idea.log&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/find-idea-log.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击菜单栏中的 &lt;strong&gt;Help&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Show Log in Explorer&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/idea-location.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择一个名为 &lt;code&gt;idea.log&lt;/code&gt; 的文件。这就是我们要找的文件。&lt;/p&gt;
&lt;p&gt;这里我的文件路径是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\obiscr\AppData\Local\JetBrains\IntelliJIdea2023.2\log\idea.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
这只是 Windows 系统的一个例子。有关日志位置的更多详细信息，请参阅 &lt;a href=&quot;https://intellij-support.jetbrains.com/hc/en-us/articles/206544519-Directories-used-by-the-IDE-to-store-settings-caches-plugins-and-logs&quot;&gt;IDE 用于存储设置、缓存、插件和日志的目录&lt;/a&gt;
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;更复杂的情况&lt;/h2&gt;
&lt;p&gt;通常，通过以上方法，问题可以解决。但有时，会有一些奇怪的情况，甚至作者也无法解决，或者可能是 IDE 本身的一个错误。所以错误修复周期不是很固定。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;当出现问题时，我们可以一次性向作者提供尽可能多的信息。例如，所有上述内容。
如果只有一部分，作者可能会要求你提供其他必要的信息。这种沟通通常需要更多时间。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
这适用于任何问题，事实上，我们可以提供尽可能多的信息，总是节省一些沟通时间，并尽快解决问题。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>如何加速 pnpm 的 postinstall</title><link>https://obiscr.com/zh-cn/blog/how-to-speed-up-pnpm-postinstall</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/how-to-speed-up-pnpm-postinstall</guid><description>本文将通过以 supabase cli 为例来介绍如何加速pnpm的postinstall。</description><content:encoded>&lt;p&gt;import { Aside, Badge, FileTree } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;如果你的项目中有使用过 supabase cli，那么你可能遇到过这样的问题：&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; 中包含如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 &lt;code&gt;pnpm install&lt;/code&gt; 后，你会看到类似如下的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) obiscr@192 my-project % pnpm install

   ╭──────────────────────────────────────────╮
   │                                          │
   │   Update available! 10.10.0 → 10.14.0.   │
   │   Changelog: https://pnpm.io/v/10.14.0   │
   │     To update, run: pnpm self-update     │
   │                                          │
   ╰──────────────────────────────────────────╯

 WARN  deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
 WARN  6 deprecated subdependencies found: @humanwhocodes/config-array@0.13.0, @humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0, rimraf@3.0.2
Packages: +417
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 WARN  Failed to create bin at /Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/bin/supabase&apos;
Progress: resolved 433, reused 382, downloaded 20, added 417, done
 WARN  Failed to create bin at /Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/supabase/bin/supabase&apos;
node_modules/supabase: Running postinstall script, failed in 3m 52.2s
node_modules/supabase postinstall$ node scripts/postinstall.js
│ Downloading https://github.com/supabase/cli/releases/download/v2.34.3/supabase_2.34.3_checksums.txt
│ Downloading https://github.com/supabase/cli/releases/download/v2.34.3/supabase_darwin_arm64.tar.gz
│ Warning: Detected unsettled top-level await at file:///Users/obiscr/EasyCodeAIProjects/myblog123/node_modules/su…
│ await main();
│ ^
└─ Failed in 3m 52.2s at /Users/obiscr/projects/my-project/node_modules/supabase
node_modules/unrs-resolver: Running postinstall script, done in 199ms
 ELIFECYCLE  Command failed with exit code 13.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过了将近4分钟，但是依旧安装失败。经过我的多次测试，这种情况在网络条件比较差的时候时有发生。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot;&amp;gt;
本文虽然使用 &amp;lt;Badge text=&quot;supabase cli&quot; variant=&quot;note&quot; /&amp;gt; 作为示例，但是这个方法具有通用性。本质就是更改 &lt;code&gt;postinstall.js&lt;/code&gt; 文件，让其在安装时优先使用本地文件，如果本地文件不存在，则执行默认逻辑从 &lt;code&gt;github&lt;/code&gt; 下载。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;p&gt;既然是 &lt;code&gt;supabase cli&lt;/code&gt; 的 &lt;code&gt;postinstall&lt;/code&gt; 脚本安装失败，那么我们来看看 &lt;code&gt;supabase cli&lt;/code&gt; 的 &lt;code&gt;postinstall&lt;/code&gt; 脚本做了什么。&lt;/p&gt;
&lt;p&gt;进入 &lt;code&gt;node_modules/supabase&lt;/code&gt; 目录，结构如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;supabase
&lt;ul&gt;
&lt;li&gt;bin/&lt;/li&gt;
&lt;li&gt;scripts/
&lt;ul&gt;
&lt;li&gt;postinstall.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LICENSE&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;li&gt;README.md
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;postinstall.js&lt;/code&gt; 中包含如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

// Ref 1: https://github.com/sanathkr/go-npm
// Ref 2: https://medium.com/xendit-engineering/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
&quot;use strict&quot;;

import binLinks from &quot;bin-links&quot;;
import { createHash } from &quot;crypto&quot;;
import fs from &quot;fs&quot;;
import fetch from &quot;node-fetch&quot;;
import { Agent } from &quot;https&quot;;
import { HttpsProxyAgent } from &quot;https-proxy-agent&quot;;
import path from &quot;path&quot;;
import { extract } from &quot;tar&quot;;
import zlib from &quot;zlib&quot;;

// Mapping from Node&apos;s `process.arch` to Golang&apos;s `$GOARCH`
const ARCH_MAPPING = {
  x64: &quot;amd64&quot;,
  arm64: &quot;arm64&quot;,
};

// Mapping between Node&apos;s `process.platform` to Golang&apos;s
const PLATFORM_MAPPING = {
  darwin: &quot;darwin&quot;,
  linux: &quot;linux&quot;,
  win32: &quot;windows&quot;,
};

const arch = ARCH_MAPPING[process.arch];
const platform = PLATFORM_MAPPING[process.platform];

// TODO: import pkg from &quot;../package.json&quot; assert { type: &quot;json&quot; };
const readPackageJson = async () =&amp;gt; {
  const contents = await fs.promises.readFile(&quot;package.json&quot;);
  return JSON.parse(contents);
};

// Build the download url from package.json
const getDownloadUrl = (packageJson) =&amp;gt; {
  const pkgName = packageJson.name;
  const version = packageJson.version;
  const repo = packageJson.repository;
  const url = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${platform}_${arch}.tar.gz`;
  return url;
};

const fetchAndParseCheckSumFile = async (packageJson, agent) =&amp;gt; {
  const version = packageJson.version;
  const pkgName = packageJson.name;
  const repo = packageJson.repository;
  const checksumFileUrl = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${version}_checksums.txt`;

  // Fetch the checksum file
  console.info(&quot;Downloading&quot;, checksumFileUrl);
  const response = await fetch(checksumFileUrl, { agent });
  if (response.ok) {
    const checkSumContent = await response.text();
    const lines = checkSumContent.split(&quot;\n&quot;);

    const checksums = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      checksums[packageName] = checksum;
    }

    return checksums;
  } else {
    console.error(
      &quot;Could not fetch checksum file&quot;,
      response.status,
      response.statusText
    );
  }
};

const errGlobal = `Installing Supabase CLI as a global module is not supported.
Please use one of the supported package managers: https://github.com/supabase/cli#install-the-cli
`;
const errChecksum = &quot;Checksum mismatch. Downloaded data might be corrupted.&quot;;
const errUnsupported = `Installation is not supported for ${process.platform} ${process.arch}`;

/**
 * Reads the configuration from application&apos;s package.json,
 * downloads the binary from package url and stores at
 * ./bin in the package&apos;s root.
 *
 *  See: https://docs.npmjs.com/files/package.json#bin
 */
async function main() {
  const yarnGlobal = JSON.parse(
    process.env.npm_config_argv || &quot;{}&quot;
  ).original?.includes(&quot;global&quot;);
  if (process.env.npm_config_global || yarnGlobal) {
    throw errGlobal;
  }
  if (!arch || !platform) {
    throw errUnsupported;
  }

  // Read from package.json and prepare for the installation.
  const pkg = await readPackageJson();
  if (platform === &quot;windows&quot;) {
    // Update bin path in package.json
    pkg.bin[pkg.name] += &quot;.exe&quot;;
  }

  // Prepare the installation path by creating the directory if it doesn&apos;t exist.
  const binPath = pkg.bin[pkg.name];
  const binDir = path.dirname(binPath);
  await fs.promises.mkdir(binDir, { recursive: true });

  // Create the agent that will be used for all the fetch requests later.
  const proxyUrl =
    process.env.npm_config_https_proxy ||
    process.env.npm_config_http_proxy ||
    process.env.npm_config_proxy;
  // Keeps the TCP connection alive when sending multiple requests
  // Ref: https://github.com/node-fetch/node-fetch/issues/1735
  const agent = proxyUrl
    ? new HttpsProxyAgent(proxyUrl, { keepAlive: true })
    : new Agent({ keepAlive: true });

  // First, fetch the checksum map.
  const checksumMap = await fetchAndParseCheckSumFile(pkg, agent);

  // Then, download the binary.
  const url = getDownloadUrl(pkg);
  console.info(&quot;Downloading&quot;, url);
  const resp = await fetch(url, { agent });
  const hash = createHash(&quot;sha256&quot;);
  const pkgNameWithPlatform = `${pkg.name}_${platform}_${arch}.tar.gz`;

  // Then, decompress the binary -- we will first Un-GZip, then we will untar.
  const ungz = zlib.createGunzip();
  const binName = path.basename(binPath);
  const untar = extract({ cwd: binDir }, [binName]);

  // Update the hash with the binary data as it&apos;s being downloaded.
  resp.body
    .on(&quot;data&quot;, (chunk) =&amp;gt; {
      hash.update(chunk);
    })
    // Pipe the data to the ungz stream.
    .pipe(ungz);

  // After the ungz stream has ended, verify the checksum.
  ungz
    .on(&quot;end&quot;, () =&amp;gt; {
      const expectedChecksum = checksumMap?.[pkgNameWithPlatform];
      // Skip verification if we can&apos;t find the file checksum
      if (!expectedChecksum) {
        console.warn(&quot;Skipping checksum verification&quot;);
        return;
      }
      const calculatedChecksum = hash.digest(&quot;hex&quot;);
      if (calculatedChecksum !== expectedChecksum) {
        throw errChecksum;
      }
      console.info(&quot;Checksum verified.&quot;);
    })
    // Pipe the data to the untar stream.
    .pipe(untar);

  // Wait for the untar stream to finish.
  await new Promise((resolve, reject) =&amp;gt; {
    untar.on(&quot;error&quot;, reject);
    untar.on(&quot;end&quot;, () =&amp;gt; resolve());
  });

  // Link the binaries in postinstall to support yarn
  await binLinks({
    path: path.resolve(&quot;.&quot;),
    pkg: { ...pkg, bin: { [pkg.name]: binPath } },
  });

  console.info(&quot;Installed Supabase CLI successfully&quot;);
}

await main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来讲，就是从 &lt;code&gt;supabase&lt;/code&gt; 的 &lt;code&gt;github&lt;/code&gt; 仓库中下载 &lt;code&gt;supabase&lt;/code&gt; 的二进制文件，并将其安装到 &lt;code&gt;node_modules&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;那么，我们是否可以优化这个过程呢？&lt;/p&gt;
&lt;p&gt;答案是肯定的。&lt;/p&gt;
&lt;p&gt;我们可以将 &lt;code&gt;supabase&lt;/code&gt; 的二进制文件下载到本地，然后将其安装到 &lt;code&gt;node_modules&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;在项目根目录终端执行下面的命令来下载文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L -o lib/supabase/supabase_2.34.3_checksums.txt https://github.com/supabase/cli/releases/download/v2.34.3/supabase_2.34.3_checksums.txt
curl -L -o lib/supabase/supabase_darwin_arm64.tar.gz https://github.com/supabase/cli/releases/download/v2.34.3/supabase_darwin_arm64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在终端执行 &lt;code&gt;pnpm patch supabase@2.34.3&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm patch supabase@2.34.3
Patch: You can now edit the package at:

  /Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3

To commit your changes, run:

  pnpm patch-commit &apos;/Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;node_modules/.pnpm_patches/supabase@2.34.3&lt;/code&gt; 目录下，你会看到如下内容：&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node_modules/
&lt;ul&gt;
&lt;li&gt;.pnpm_patches
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3
&lt;ul&gt;
&lt;li&gt;scripts
&lt;ul&gt;
&lt;li&gt;postinstall.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LICENSE&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;li&gt;README.md&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;state.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后编辑 &lt;code&gt;postinstall.js&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env node

// Ref 1: https://github.com/sanathkr/go-npm
// Ref 2: https://medium.com/xendit-engineering/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
&quot;use strict&quot;;

import binLinks from &quot;bin-links&quot;;
import { createHash } from &quot;crypto&quot;;
import fs from &quot;fs&quot;;
import fetch from &quot;node-fetch&quot;;
import { Agent } from &quot;https&quot;;
import { HttpsProxyAgent } from &quot;https-proxy-agent&quot;;
import path from &quot;path&quot;;
import { extract } from &quot;tar&quot;;
import zlib from &quot;zlib&quot;;

// Mapping from Node&apos;s `process.arch` to Golang&apos;s `$GOARCH`
const ARCH_MAPPING = {
  x64: &quot;amd64&quot;,
  arm64: &quot;arm64&quot;,
};

// Mapping between Node&apos;s `process.platform` to Golang&apos;s
const PLATFORM_MAPPING = {
  darwin: &quot;darwin&quot;,
  linux: &quot;linux&quot;,
  win32: &quot;windows&quot;,
};

const arch = ARCH_MAPPING[process.arch];
const platform = PLATFORM_MAPPING[process.platform];

// TODO: import pkg from &quot;../package.json&quot; assert { type: &quot;json&quot; };
const readPackageJson = async () =&amp;gt; {
  const contents = await fs.promises.readFile(&quot;package.json&quot;);
  return JSON.parse(contents);
};

// Build the download url from package.json
const getDownloadUrl = (packageJson) =&amp;gt; {
  const pkgName = packageJson.name;
  const version = packageJson.version;
  const repo = packageJson.repository;
  const url = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${platform}_${arch}.tar.gz`;
  return url;
};

const fetchAndParseCheckSumFile = async (packageJson, agent) =&amp;gt; {
  const version = packageJson.version;
  const pkgName = packageJson.name;
  const repo = packageJson.repository;
  const checksumFileUrl = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${version}_checksums.txt`;

  // Fetch the checksum file
  console.info(&quot;Downloading&quot;, checksumFileUrl);
  const response = await fetch(checksumFileUrl, { agent });
  if (response.ok) {
    const checkSumContent = await response.text();
    const lines = checkSumContent.split(&quot;\n&quot;);

    const checksums = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      checksums[packageName] = checksum;
    }

    return checksums;
  } else {
    console.error(
      &quot;Could not fetch checksum file&quot;,
      response.status,
      response.statusText
    );
  }
};

const errGlobal = `Installing Supabase CLI as a global module is not supported.
Please use one of the supported package managers: https://github.com/supabase/cli#install-the-cli
`;
const errChecksum = &quot;Checksum mismatch. Downloaded data might be corrupted.&quot;;
const errUnsupported = `Installation is not supported for ${process.platform} ${process.arch}`;

/**
 * Reads the configuration from application&apos;s package.json,
 * downloads the binary from package url and stores at
 * ./bin in the package&apos;s root.
 *
 *  See: https://docs.npmjs.com/files/package.json#bin
 */
async function main() {
  const yarnGlobal = JSON.parse(
    process.env.npm_config_argv || &quot;{}&quot;
  ).original?.includes(&quot;global&quot;);
  if (process.env.npm_config_global || yarnGlobal) {
    throw errGlobal;
  }
  if (!arch || !platform) {
    throw errUnsupported;
  }

  // Read from package.json and prepare for the installation.
  const pkg = await readPackageJson();
  if (platform === &quot;windows&quot;) {
    // Update bin path in package.json
    pkg.bin[pkg.name] += &quot;.exe&quot;;
  }

  // Prepare the installation path by creating the directory if it doesn&apos;t exist.
  const binPath = pkg.bin[pkg.name];
  const binDir = path.dirname(binPath);
  await fs.promises.mkdir(binDir, { recursive: true });

  // Create the agent that will be used for all the fetch requests later.
  const proxyUrl =
    process.env.npm_config_https_proxy ||
    process.env.npm_config_http_proxy ||
    process.env.npm_config_proxy;
  // Keeps the TCP connection alive when sending multiple requests
  // Ref: https://github.com/node-fetch/node-fetch/issues/1735
  const agent = proxyUrl
    ? new HttpsProxyAgent(proxyUrl, { keepAlive: true })
    : new Agent({ keepAlive: true });

  // First, fetch the checksum map.
  const checksumMap = await fetchAndParseCheckSumFile(pkg, agent);
  // Prefer local resources if present (project root via INIT_CWD)
  const projectRoot = process.env.INIT_CWD || process.cwd();
  const resourcesDir = path.join(projectRoot, &quot;lib&quot;, &quot;supabase&quot;);
  const localChecksumPath = path.join(resourcesDir, `${pkg.name}_${pkg.version}_checksums.txt`);
  const localTgzPath = path.join(resourcesDir, `${pkg.name}_${platform}_${arch}.tar.gz`);
  
  // First, read checksum map from local file if available; otherwise fetch
  let checksumMap;
  try {
    const checkSumContent = await fs.promises.readFile(localChecksumPath, &quot;utf8&quot;);
    const lines = checkSumContent.split(&quot;\n&quot;);
    checksumMap = {};
    for (const line of lines) {
      const [checksum, packageName] = line.split(/\s+/);
      if (packageName) checksumMap[packageName] = checksum;
    }
    console.info(&quot;Using local checksum file&quot;, localChecksumPath);
  } catch {
    // fallback to remote checksum
    checksumMap = await fetchAndParseCheckSumFile(pkg, agent);
  }

  // Then, download the binary.
  const url = getDownloadUrl(pkg);
  console.info(&quot;Downloading&quot;, url);
  const resp = await fetch(url, { agent });
  const hash = createHash(&quot;sha256&quot;);
  const pkgNameWithPlatform = `${pkg.name}_${platform}_${arch}.tar.gz`;

  // Then, decompress the binary -- we will first Un-GZip, then we will untar.
  const ungz = zlib.createGunzip();
  const binName = path.basename(binPath);
  const untar = extract({ cwd: binDir }, [binName]);

  // Update the hash with the binary data as it&apos;s being downloaded.
  resp.body
  let sourceStream;
  let usingLocal = false;
  try {
    await fs.promises.access(localTgzPath, fs.constants.R_OK);
    usingLocal = true;
  } catch {}
  if (usingLocal) {
    console.info(&quot;Using local tgz&quot;, localTgzPath);
    sourceStream = fs.createReadStream(localTgzPath);
  } else {
    console.info(&quot;Downloading&quot;, url);
    const resp = await fetch(url, { agent });
    sourceStream = resp.body;
  }
  sourceStream
    .on(&quot;data&quot;, (chunk) =&amp;gt; {
      hash.update(chunk);
    })
    // Pipe the data to the ungz stream.
    .pipe(ungz);

  // After the ungz stream has ended, verify the checksum.
  ungz
    .on(&quot;end&quot;, () =&amp;gt; {
      const expectedChecksum = checksumMap?.[pkgNameWithPlatform];
      // Skip verification if we can&apos;t find the file checksum
      if (!expectedChecksum) {
        console.warn(&quot;Skipping checksum verification&quot;);
        return;
      }
      const calculatedChecksum = hash.digest(&quot;hex&quot;);
      if (calculatedChecksum !== expectedChecksum) {
        throw errChecksum;
      }
      console.info(&quot;Checksum verified.&quot;);
    })
    // Pipe the data to the untar stream.
    .pipe(untar);

  // Wait for the untar stream to finish.
  await new Promise((resolve, reject) =&amp;gt; {
    untar.on(&quot;error&quot;, reject);
    untar.on(&quot;end&quot;, () =&amp;gt; resolve());
  });

  // Link the binaries in postinstall to support yarn
  await binLinks({
    path: path.resolve(&quot;.&quot;),
    pkg: { ...pkg, bin: { [pkg.name]: binPath } },
  });

  console.info(&quot;Installed Supabase CLI successfully&quot;);
}

await main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增的这部分逻辑，会优先使用 &lt;code&gt;lib/supabase&lt;/code&gt; 目录下的文件，如果文件不存在，则执行默认逻辑从 &lt;code&gt;github&lt;/code&gt; 下载。&lt;/p&gt;
&lt;p&gt;编辑完成以后，我们提交更改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm patch-commit &apos;/Users/obiscr/projects/my-project/node_modules/.pnpm_patches/supabase@2.34.3&apos;
Lockfile is up to date, resolution step is skipped
Packages: +20
++++++++++++++++++++
Progress: resolved 0, reused 45, downloaded 0, added 20, done
node_modules/supabase: Running postinstall script, done in 236ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将会在项目根目录的 &lt;code&gt;patches&lt;/code&gt; 目录下生成 &lt;code&gt;supabase@2.34.3.patch&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;lib/
&lt;ul&gt;
&lt;li&gt;supabase/
&lt;ul&gt;
&lt;li&gt;supabase_2.34.3_checksums.txt&lt;/li&gt;
&lt;li&gt;supabase_darwin_arm64.tar.gz&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;patches/
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还会在 &lt;code&gt;package.json&lt;/code&gt; 生成 &lt;code&gt;patchedDependencies&lt;/code&gt; 配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    }
    },
    &quot;patchedDependencies&quot;: {
      &quot;supabase@2.34.3&quot;: &quot;patches/supabase@2.34.3.patch&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们把 &lt;code&gt;supabase@2.34.3.patch&lt;/code&gt; 文件移动到 &lt;code&gt;lib/supabase&lt;/code&gt; 目录中。&lt;/p&gt;
&lt;p&gt;此时目录结构如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my-project
&lt;ul&gt;
&lt;li&gt;lib/
&lt;ul&gt;
&lt;li&gt;supabase/
&lt;ul&gt;
&lt;li&gt;supabase_2.34.3_checksums.txt&lt;/li&gt;
&lt;li&gt;supabase_darwin_arm64.tar.gz&lt;/li&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;patches/
&lt;ul&gt;
&lt;li&gt;supabase@2.34.3.patch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;package.json
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时我们更改 &lt;code&gt;package.json&lt;/code&gt; 中的 &lt;code&gt;patchedDependencies&lt;/code&gt; 配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev --turbo&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;,
    &quot;type-check&quot;: &quot;tsc --noEmit&quot;,
    &quot;check-all&quot;: &quot;npm run lint &amp;amp; npm run type-check &amp;amp; wait&quot;
  },
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^18&quot;,
    &quot;react-dom&quot;: &quot;^18&quot;,
    &quot;next&quot;: &quot;14.2.23&quot;,
    &quot;@supabase/ssr&quot;: &quot;^0.6.1&quot;,
    &quot;@supabase/supabase-js&quot;: &quot;^2.49.4&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;typescript&quot;: &quot;^5&quot;,
    &quot;@types/node&quot;: &quot;^20&quot;,
    &quot;@types/react&quot;: &quot;^18&quot;,
    &quot;@types/react-dom&quot;: &quot;^18&quot;,
    &quot;postcss&quot;: &quot;^8&quot;,
    &quot;supabase&quot;: &quot;^2.34.3&quot;,
    &quot;tailwindcss&quot;: &quot;^3.4.1&quot;,
    &quot;eslint&quot;: &quot;^8&quot;,
    &quot;eslint-config-next&quot;: &quot;14.2.23&quot;
  },
  &quot;pnpm&quot;: {
    &quot;onlyBuiltDependencies&quot;: [
      &quot;supabase&quot;,
      &quot;unrs-resolver&quot;
    ],
    &quot;peerDependencyRules&quot;: {
      &quot;allowAny&quot;: [
        &quot;supabase&quot;
      ]
    },
    &quot;patchedDependencies&quot;: {
      &quot;supabase@2.34.3&quot;: &quot;patches/supabase@2.34.3.patch&quot;
      &quot;supabase@2.34.3&quot;: &quot;lib/supabase/supabase@2.34.3.patch&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试更改&lt;/h2&gt;
&lt;p&gt;删除 &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;，然后运行 &lt;code&gt;pnpm install&lt;/code&gt; ，你应该能够看到安装速度明显加快。只需要 &lt;em&gt;266ms&lt;/em&gt; 即可完成安装。相较于之前的接近4分钟，有了非常明显的提升。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;obiscr@192 my-project % pnpm install
 WARN  deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
 WARN  6 deprecated subdependencies found: @humanwhocodes/config-array@0.13.0, @humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, node-domexception@1.0.0, rimraf@3.0.2
Packages: +418
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 WARN  Failed to create bin at /Users/obiscr/projects/my-project/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/projects/my-project/node_modules/supabase/bin/supabase&apos;
Progress: resolved 433, reused 402, downloaded 0, added 418, done
 WARN  Failed to create bin at /Users/obiscr/projects/my-project/node_modules/supabase/node_modules/.bin/supabase. ENOENT: no such file or directory, chmod &apos;/Users/obiscr/projects/my-project/node_modules/supabase/bin/supabase&apos;
node_modules/supabase: Running postinstall script, done in 266ms
Done in 1m 25.2s using pnpm v10.10.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文介绍了如何通过 &lt;code&gt;pnpm patch&lt;/code&gt; 命令来加速 &lt;code&gt;pnpm&lt;/code&gt; 的 &lt;code&gt;postinstall&lt;/code&gt; 脚本。希望对你有用。&lt;/p&gt;
</content:encoded></item><item><title>Intellij 自适应颜色方案</title><link>https://obiscr.com/zh-cn/blog/intellij-adaptive-color-scheme</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/intellij-adaptive-color-scheme</guid><description>用户界面组件的颜色适应 IDE 主题变化。</description><content:encoded>&lt;p&gt;Intellij 自适应颜色方案&lt;/p&gt;
&lt;p&gt;本文描述了如何使 IntelliJ 中的用户界面组件的颜色适应 IDE 主题的变化。&lt;/p&gt;
&lt;h2&gt;创建一个 UI 组件&lt;/h2&gt;
&lt;p&gt;假设我们现在有一个像这样的组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们为 &lt;code&gt;UIContainer&lt;/code&gt; 设置了一个背景颜色，它来自当前 &lt;strong&gt;ColorsScheme&lt;/strong&gt; 的 &lt;code&gt;NOTIFICATION_BACKGROUND&lt;/code&gt;，
但是很明显，在这种情况下，当我们切换主题时，&lt;code&gt;UIContainer&lt;/code&gt; 的颜色不会与主题的颜色一起切换。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UIContainer&lt;/code&gt; 的颜色不会在切换主题时与主题的颜色一起切换。然而，在大多数情况下，
这可能会导致 UI 具有相似的颜色和字体，因此显示的内容不可见。&lt;/p&gt;
&lt;p&gt;那么，我们如何让 &lt;code&gt;UIContainer&lt;/code&gt; 的背景颜色随着主题的变化而自动变化呢？&lt;/p&gt;
&lt;h2&gt;适应 IDE 主题&lt;/h2&gt;
&lt;h3&gt;实现接口&lt;/h3&gt;
&lt;p&gt;让 &lt;code&gt;UIContainer&lt;/code&gt; 实现 Disposable 和 EditorColorsListener&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {
    {... rest of code}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加消息事件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)

      MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(this);
      connection.subscribe(EditorColorsManager.TOPIC, this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现 globalSchemeChange&lt;/h3&gt;
&lt;p&gt;当主题改变时，&lt;strong&gt;globalSchemeChange&lt;/strong&gt; 方法将被调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;currentColorsScheme&lt;/strong&gt; 将是选定的主题，因此我们从 &lt;strong&gt;currentColorsScheme&lt;/strong&gt; 中重新获得 &lt;code&gt;notificationColor&lt;/code&gt; 并重置组件的颜色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    {... rest of code}

    @Override
    public void globalSchemeChange(@Nullable EditorColorsScheme currentColorsScheme) {
        Color color = currentColorsScheme == null ? scheme.getColor(notificationColor) :
                currentColorsScheme.getColor(notificationColor);
        setBackground(color);
        revalidate();
        repaint();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现 dispose&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    {... rest of code}

    @Override
    public void dispose() {
        Disposer.dispose(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Finally Code&lt;/h2&gt;
&lt;p&gt;那么，最终的代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UIContainer extends JBPanel&amp;lt;UIContainer&amp;gt; implements Disposable, EditorColorsListener {

    private static final EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
    private static EditorColorsScheme scheme = editorColorsManager.getGlobalScheme();
    private static final ColorKey notificationColor =  ColorKey.createColorKey(&quot;NOTIFICATION_BACKGROUND&quot;);

    public MessageComponent() {
      // TODO init some actions...

      Color color = scheme.getColor(notificationColor);
      setBackground(color)

      MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(this);
      connection.subscribe(EditorColorsManager.TOPIC, this);
    }

    @Override
    public void globalSchemeChange(@Nullable EditorColorsScheme currentColorsScheme) {
        if (currentColorsScheme == null) {
            return;
        }
        Color color = scheme.getColor(notificationColor);
        setBackground(color);
        revalidate();
        repaint();
    }

    @Override
    public void dispose() {
        Disposer.dispose(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在当你切换主题时，组件的颜色会自动与主题的颜色一起变化。希望这对你有帮助。&lt;/p&gt;
</content:encoded></item><item><title>IntelliJ 平台中的 Disposer</title><link>https://obiscr.com/zh-cn/blog/intellij-disposer</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/intellij-disposer</guid><description>本文是一个案例研究，旨在简化 IntelliJ 平台，并介绍 IntelliJ 平台中的 Disposer。</description><content:encoded>&lt;p&gt;本文是一个案例研究，旨在简化 IntelliJ 平台，并介绍 IntelliJ 平台中的 Disposer。&lt;/p&gt;
&lt;p&gt;事实上，SDK 文档已经对这一点进行了更详细的描述：&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/disposers.html&quot;&gt;Disposer 和 Disposable&lt;/a&gt;，在开始之前，最好先看一下这里。&lt;/p&gt;
&lt;h2&gt;场景描述&lt;/h2&gt;
&lt;p&gt;我在这里模拟了一个场景。有一个 ToolWindow 布局如下，这里我们使用 &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/internal-ui-inspector.html&quot;&gt;UI Inspector&lt;/a&gt; 来分析布局：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/toolwindow-layout.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/dispose-tool-window-layout.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个带有 BorderLayout 的 rootPanel，顶部（North）是一个 Toolbar，中心（Center）是一个 JPanel。点击 ➕ 将添加一个随机代码片段到 Center 的 JPanel。&lt;/p&gt;
&lt;p&gt;场景如上所示，是一个非常简单的 ToolWindow，功能也非常简单。&lt;/p&gt;
&lt;h2&gt;内存泄漏&lt;/h2&gt;
&lt;p&gt;这是代码片段的关键部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.obiscr.template.dispose.MainPanel;
import org.jetbrains.annotations.NotNull;

public class MyToolWindowFactory implements ToolWindowFactory {
    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        ContentFactory contentFactory = ContentFactory.getInstance();
        MainPanel panel = new MainPanel(project);
        Content content = contentFactory.createContent(panel.getComponent(), &quot;Dispose&quot;, false);
        toolWindow.getContentManager().addContent(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.project.Project;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.components.panels.VerticalLayout;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

public class MainPanel {

    private final JPanel rootPanel;
    private final JPanel centerPanel;
    private final Project myProject;


    public MainPanel(Project project) {
        myProject = project;
        rootPanel = new JPanel(new BorderLayout());
        centerPanel = new JPanel(new VerticalLayout(JBUI.scale(10)));
        centerPanel.setBorder(JBUI.Borders.empty(10));

        DefaultActionGroup actionGroup = new DefaultActionGroup();
        AnAction addAction = new AnAction(() -&amp;gt; &quot;Add Item&quot;, AllIcons.General.Add) {
            @Override
            public void actionPerformed(@NotNull AnActionEvent e) {
                String text = &quot;// Code snippet at &quot; + LocalDateTime.now().
                format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;)) + &quot;\n&quot;;
                text += HelloWorld.getRandomHelloWorldCode();
                Editor editor = createEditor(myProject, text);
                centerPanel.add(editor.getComponent());
                centerPanel.revalidate();
                centerPanel.repaint();
            }
        };
        actionGroup.add(addAction);
        ActionToolbarImpl actionToolbar = new ActionToolbarImpl(&quot;MyPanelToolbar&quot;, actionGroup, true);
        actionToolbar.setTargetComponent(rootPanel);
        actionToolbar.setBorder(JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1,0,1,0));
        rootPanel.add(actionToolbar, BorderLayout.NORTH);
        rootPanel.add(centerPanel, BorderLayout.CENTER);
    }

    public JPanel getComponent() {
        return rootPanel;
    }

    private Editor createEditor(Project project, String text) {
        EditorFactory editorFactory = EditorFactory.getInstance();
        EditorEx editor = (EditorEx) editorFactory.createViewer(
                editorFactory.createDocument(text),
                project);
        // Others settings
        // ...
        return editor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们只是创建了 Editor 对象，但当程序退出时，我们没有清理它占用的内存。&lt;/p&gt;
&lt;p&gt;当关闭 IDE 时，会显示相关异常消息（当然，这些信息也会显示在 &lt;code&gt;idea.log&lt;/code&gt; 中）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/console-stacktrace.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/console-stacktrace-code.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据错误日志，我们可以知道这是因为创建的 Editor 对象在应用程序关闭时没有释放，存在内存溢出的风险。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/console-code-preview.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;好的，现在我们已经知道问题所在，接下来我们解决这个问题，解决方案也很简单，只需要关闭 IDE，释放这个对象即可。&lt;/p&gt;
&lt;p&gt;首先，让 MainPanel 实现 &lt;strong&gt;Disposable&lt;/strong&gt; 接口并重写 &lt;code&gt;dispose()&lt;/code&gt; 方法。然后，在创建 &lt;code&gt;editor&lt;/code&gt; 后，将 &lt;code&gt;Disposer&lt;/code&gt; 注册到父 &lt;code&gt;this&lt;/code&gt; 中，表示 &lt;strong&gt;当这个对象被释放时，Editor 将被销毁&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MainPanel implements Disposable {
	@Override
    public void dispose() {
        centerPanel.removeAll();
        rootPanel.removeAll();
    }

    private Editor createEditor(Project project, String text) {
        EditorEx editor = ...;
        Disposer.register(this, () -&amp;gt; EditorFactory.getInstance().releaseEditor(editor));
        return editor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，由于在这种情况下组件都基于 Content，我们还将 MainPanel 注册为 Content 的 Disposer。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyToolWindowFactory implements ToolWindowFactory {

    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        MainPanel panel = new MainPanel(project);
        Content content = ...;
        content.setDisposer(panel);
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ContentImpl.java 的源代码如下，你可以看到当 &lt;code&gt;content&lt;/code&gt; 被销毁时，它会调用 &lt;code&gt;Disposer.dispose(myDisposer);&lt;/code&gt; 来销毁设置的 myDisposer，也就是上面的 MainPanel。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @Override
  public void dispose() {
    if (myShouldDisposeContent &amp;amp;&amp;amp; myComponent instanceof Disposable) {
      Disposer.dispose((Disposable)myComponent);
    }

    if (myDisposer != null) {
      Disposer.dispose(myDisposer);
      myDisposer = null;
    }

    myFocusRequest = null;
    clearUserData();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么现在整个过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;content.dispose() --(call)--&amp;gt; panel.dispose() --(call)--&amp;gt; EditorFactory.getInstance().releaseEditor(editor);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，当 &lt;code&gt;content&lt;/code&gt; 被关闭时，Editor 对象也会被销毁。&lt;/p&gt;
</content:encoded></item><item><title>IntelliJ 插件开发</title><link>https://obiscr.com/zh-cn/blog/intellij-plugin-development</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/intellij-plugin-development</guid><description>本文介绍了插件开发相关内容，重点是如何处理遇到的...</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;本文介绍了插件开发相关内容，重点是如何处理遇到的...&lt;/p&gt;
&lt;p&gt;我相信，许多插件开发者在初期会遇到很多各种困难，但这是很正常的。到目前为止，我已经开发了 &lt;a href=&quot;https://plugins.jetbrains.com/organizations/obiscr&quot;&gt;许多插件&lt;/a&gt;，在此期间也遇到了很多问题。有些还在学习，有些还在解决。如果你有类似的问题，那么这篇文章值得一读。&lt;/p&gt;
&lt;p&gt;For myself, when it comes to problems, there are generally so many ways to deal with them as follows:&lt;/p&gt;
&lt;h2&gt;I - IntelliJ 平台&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/intellij-platform.html&quot;&gt;IntelliJ 平台&lt;/a&gt; 相当于 SDK 平台文档。包括插件、基础平台、项目模型、PSI、功能、测试、资源、API 变化、工具和其他模块的文档。&lt;/p&gt;
&lt;h2&gt;II - IntelliJ 社区&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://intellij-support.jetbrains.com/hc/en-us/community/topics/200382555-IntelliJ-IDEA-Users&quot;&gt;IntelliJ 社区&lt;/a&gt; 包含很多问题和答案。&lt;/p&gt;
&lt;h2&gt;III - IntelliJ 插件&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-plugins&quot;&gt;IntelliJ 插件&lt;/a&gt; 是一个包含许多 JetBrains 自己插件的 git 仓库，你可以在这里找到好的示例进行参考。&lt;/p&gt;
&lt;h2&gt;IV - IntelliJ 代码示例&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-sdk-code-samples&quot;&gt;IntelliJ 代码示例&lt;/a&gt; 是一个包含许多好的示例的 git 仓库，你可以在这里找到好的示例进行参考。&lt;/p&gt;
&lt;h2&gt;V - YouTrack&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issues&quot;&gt;YouTrack&lt;/a&gt; 主要用于反馈 bug，你也可以在这里搜索相关信息，当你遇到问题时。&lt;/p&gt;
&lt;h2&gt;VI - 内部模型 &amp;amp; 源代码&lt;/h2&gt;
&lt;p&gt;这是一个非常有用的功能。内部模式可以从 &lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/enabling-internal.html&quot;&gt;这个文档&lt;/a&gt; 中启用。内部模式与 &lt;a href=&quot;https://github.com/JetBrains/intellij-community&quot;&gt;IntelliJ 社区版源代码&lt;/a&gt; 一起可以解决很多问题。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
我非常推荐使用这种方法来解决问题。使用这种方法时，你需要阅读 IntelliJ 社区版源代码，这对你养成阅读源代码的习惯非常有帮助。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;VII - Slack&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://jetbrains-platform.slack.com/&quot;&gt;这里&lt;/a&gt; 你可以讨论 JetBrains IDE 和 Team Tools 的插件开发，以及所有与 JetBrains Marketplace 相关的事情。这里有很多插件开发者和官方开发者，大多数问题都可以在这里解决。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
Slack for me is generally the final option when I&apos;m at the end of my rope, and when none of the above can be solved, I look for other developers or official developers in Slack to assist with the solution.
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这些是我过去几年在开发过程中遇到的问题的解决方案。我希望它能为需要它的开发者提供参考。&lt;/p&gt;
</content:encoded></item><item><title>JetBrains Git Client 2025.3 EAP</title><link>https://obiscr.com/zh-cn/blog/jetbrains-git-client-2025-3-eap</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/jetbrains-git-client-2025-3-eap</guid><description>今天，我想分享一些非常特别的内容。最近，我很幸运地获得了 JetBrains 新 GitClient 产品的封闭预览。值得注意的是，现在应用程序渠道已经关闭，因此今天的内容相对独家。</description><content:encoded>&lt;p&gt;import {
LinkCard,
} from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://lp.jetbrains.com/closed-preview-for-jetbrains-git-client&quot;
title=&quot;Git Client from JetBrains — Closed Preview&quot;
description=&quot;The Closed Preview is your chance to get early access to JetBrains Git Client.&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;今天，我想分享一些非常特别的内容。最近，我很幸运地获得了 JetBrains 新 GitClient 产品的封闭预览。值得注意的是，现在应用程序渠道已经关闭，因此今天的内容相对独家。&lt;/p&gt;
&lt;h2&gt;开始视图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/jetbrains-git-client-startup-page.png&quot; alt=&quot;jetbrains-git-client-startup-page.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;首先，当我们打开工具时，我们可以选择登录 Github 或 GitLab，克隆一个项目，或打开一个现有项目。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/jetbrains-git-client-commit-list.png&quot; alt=&quot;jetbrains-git-client-commit-list.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开项目后，我们可以看到整体 UI 仍然很干净，许多不必要的工具窗口已被删除。&lt;/p&gt;
&lt;h2&gt;设置视图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/jetbrains-git-client-settings-view.png&quot; alt=&quot;jetbrains-git-client-settings-view.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另一个重要区别是插件系统消失了。这是一个很大的区别，我希望以后能添加回来。&lt;/p&gt;
&lt;h2&gt;合并视图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/jetbrains-git-client-merge.png&quot; alt=&quot;jetbrains-git-client-merge.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在代码合并方面，整体 UI 和行为基本上与 IDE 的内置 Git 工具一致。&lt;/p&gt;
</content:encoded></item><item><title>JIT IntelliJ 基于的 Git 客户端</title><link>https://obiscr.com/zh-cn/blog/jit-client</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/jit-client</guid><description>JetBrains 家族产品的 Git 客户端可能是我使用过的最好的 Git 客户端。所以我计划将 Git 功能作为单独的 Git 工具维护。</description><content:encoded>&lt;p&gt;import { Tabs, TabItem, Card } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;h2&gt;起源&lt;/h2&gt;
&lt;p&gt;大家好，我正在基于 &lt;a href=&quot;https://github.com/JetBrains/intellij-community&quot;&gt;intellij-commiunity&lt;/a&gt; 重新制作这个项目。只有一小部分已经完成。&lt;/p&gt;
&lt;p&gt;我将其命名为 Jit（来自 IntelliJ Git / JetBrains Git）并绘制了新的图标。你可以像在 IntelliJ IDEA 中一样打开一个项目，Git 工具窗口将在你打开后直接出现。&lt;/p&gt;
&lt;p&gt;这一切都始于 YouTrack 上的一个问题：&lt;a href=&quot;https://youtrack.jetbrains.com/issue/IDEA-152437/Make-git-client-a-standalone-app&quot;&gt;Make git client a standalone app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我认为这非常有趣。JetBrains 家族产品的 Git 客户端可能是我使用过的最好的 Git 客户端。所以我计划将 Git 功能作为单独的 Git 工具维护。&lt;/p&gt;
&lt;h2&gt;总体思路&lt;/h2&gt;
&lt;p&gt;我暂时删除了整个 Editor 部分，只保留了一些相关的工具窗口：（Git / Commit / Pull Requests / Terminal / Notification）。&lt;/p&gt;
&lt;p&gt;这是一项很大的工作，正如我之前提到的，需要处理许多模块之间的依赖关系。我工作很忙，通常只有很少的时间用于此。但说实话，我对这个很感兴趣。&lt;/p&gt;
&lt;h2&gt;更新&lt;/h2&gt;
&lt;p&gt;这是一项很大的工作，正如我之前提到的，需要处理许多模块之间的依赖关系。我工作很忙，通常只有很少的时间用于此。但说实话，我对这个很感兴趣。&lt;/p&gt;
&lt;h2&gt;预览&lt;/h2&gt;
&lt;p&gt;这里有一些当前可用的预览图像。&lt;/p&gt;
&lt;p&gt;&amp;lt;Tabs&amp;gt;
&amp;lt;TabItem label=&quot;Welcome&quot;&amp;gt;
启动后，就像 IntelliJ IDEA 一样，你可以选择一个项目来打开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/welcome.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;About&quot;&amp;gt;
在 About 中，你可以看到基本信息。名称和图标已经在这里更改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/about.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Git&quot;&amp;gt;
Git 工具窗口显示有关存储库、分支和提交的基本信息，你可以通过选择提交来查看文件更改的详细信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/git-view.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Commit&quot;&amp;gt;
在 Commit 工具窗口中，你可以看到要提交的文件并输入提交信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/commit-view.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Terminal&quot;&amp;gt;
它还保留了终端工具窗口，你可以使用 git 命令进行更复杂的操作或其他基本操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/terminal.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Pull Requests&quot;&amp;gt;
在 Pull Request 工具窗口中，你可以看到存储库的当前合并请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/pull-requests.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Notification&quot;&amp;gt;
通知也保留了更好的交互体验。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![](../../../../assets/blog/images/notification.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;/Tabs&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Next.js Stripe Supabase Starter - 一个完整的电子商务演示</title><link>https://obiscr.com/zh-cn/blog/nextjs-stripe-supabase-starter</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/nextjs-stripe-supabase-starter</guid><description>一个完整的电子商务演示，使用 Next.js、Stripe Checkout 和 Supabase，具有用户身份验证、产品管理、安全支付处理和自动购买跟踪。</description><content:encoded>&lt;p&gt;import {
Steps,
Card,
FileTree,
Tabs,
TabItem,
LinkCard,
CardGrid,
} from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/nextjs-stripe-supabase-starter.git&quot;
title=&quot;nextjs-stripe-supabase-starter&quot;
description=&quot;一个基于Next.js、Stripe Checkout和Supabase构建的完整电子商务演示系统，包含用户认证、产品管理、安全支付处理及自动购买跟踪功能。&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://nextjs-stripe-supabase-starter.vercel.app/&quot;
title=&quot;在线案例&quot;
description=&quot;Next.js、Stripe 和 Supabase 启动模板的实时演示&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户身份验证&lt;/strong&gt;：使用 Supabase Auth 的电子邮件/密码登录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 个产品类别&lt;/strong&gt;：高级会员、云存储和 API 访问&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多种定价选项&lt;/strong&gt;：一次性支付和订阅&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stripe Checkout 集成&lt;/strong&gt;：安全的托管支付页面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库集成&lt;/strong&gt;：Supabase 用于用户数据和购买记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Webhook 处理&lt;/strong&gt;：自动支付确认和数据库更新&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;购买跟踪&lt;/strong&gt;：从数据库获取实时购买状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应式设计&lt;/strong&gt;：移动友好界面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;：完整的类型安全&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;：Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;：Supabase (PostgreSQL)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份验证&lt;/strong&gt;：Supabase Auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付&lt;/strong&gt;：Stripe Checkout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式&lt;/strong&gt;：Tailwind CSS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语言&lt;/strong&gt;：TypeScript&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：Vercel (推荐)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;产品结构&lt;/h2&gt;
&lt;p&gt;&amp;lt;CardGrid cols={3}&amp;gt;
&amp;lt;Card title=&quot;Premium Membership&quot;&amp;gt;
- &lt;strong&gt;月度计划&lt;/strong&gt;：$29.99
- &lt;strong&gt;年度计划&lt;/strong&gt;：$99.99
- &lt;strong&gt;终身计划&lt;/strong&gt;：$199.99
&amp;lt;/Card&amp;gt;
&amp;lt;Card title=&quot;Cloud Storage&quot;&amp;gt;
- &lt;strong&gt;50GB 存储&lt;/strong&gt;：$9.99
- &lt;strong&gt;100GB 存储&lt;/strong&gt;：$19.99
- &lt;strong&gt;500GB 存储&lt;/strong&gt;：$49.99
&amp;lt;/Card&amp;gt;
&amp;lt;Card title=&quot;API Access&quot;&amp;gt;
- &lt;strong&gt;基本 API&lt;/strong&gt;：$19.99
- &lt;strong&gt;专业 API&lt;/strong&gt;：$49.99
- &lt;strong&gt;企业 API&lt;/strong&gt;：$99.99
&amp;lt;/Card&amp;gt;
&amp;lt;/CardGrid&amp;gt;&lt;/p&gt;
&lt;h2&gt;完整设置指南&lt;/h2&gt;
&lt;p&gt;按照以下步骤从零开始设置项目：&lt;/p&gt;
&lt;h3&gt;步骤 1：克隆并安装依赖项&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 克隆仓库
git clone https://github.com/obiscr/nextjs-stripe-supabase-starter.git
cd nextjs-stripe-supabase-starter

# 安装依赖
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 2：创建 .env.local 文件&lt;/h3&gt;
&lt;p&gt;在根目录下创建一个 &lt;code&gt;.env.local&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;&amp;lt;FileTree&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nextjs-stripe-supabase-starter
&lt;ul&gt;
&lt;li&gt;src/&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.env.local&lt;/strong&gt; create this file&lt;/li&gt;
&lt;li&gt;...
&amp;lt;/FileTree&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤 3：创建 Supabase 账户&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;前往 &lt;a href=&quot;https://supabase.com&quot;&gt;Supabase&lt;/a&gt; 并创建一个免费账户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建一个组织和一个新项目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前往 &lt;strong&gt;项目概览&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前往 &lt;strong&gt;项目概览&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;Project URL&lt;/code&gt; / &lt;code&gt;API Key&lt;/code&gt; 复制到 &lt;em&gt;.env.local&lt;/em&gt; 文件中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Supabase 密钥 (从 Supabase 项目仪表板 &amp;gt; 项目设置 &amp;gt; API 密钥获取)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前往 &lt;strong&gt;项目设置&lt;/strong&gt; &amp;gt; &lt;strong&gt;API 密钥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前往 &lt;strong&gt;项目设置&lt;/strong&gt; &amp;gt; &lt;strong&gt;API 密钥&lt;/strong&gt;
将 &lt;code&gt;service_role&lt;/code&gt; 密钥复制到 &lt;em&gt;.env.local&lt;/em&gt; 文件中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Supabase 密钥 (从 Supabase 项目仪表板 &amp;gt; 项目设置 &amp;gt; API 密钥获取)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Card title=&quot;Note&quot; icon=&quot;threads&quot; &amp;gt;
请保持 &lt;code&gt;SERVICE_ROLE_KEY&lt;/code&gt; 私密，不要与任何人分享。
&amp;lt;/Card&amp;gt;
&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤 4：创建 Stripe 账户&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;前往 &lt;a href=&quot;https://stripe.com&quot;&gt;Stripe&lt;/a&gt; 并创建一个免费账户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成账户设置流程&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;前往 &lt;a href=&quot;https://dashboard.stripe.com&quot;&gt;Stripe 仪表板&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;导航到 &lt;strong&gt;开发者&lt;/strong&gt; &amp;gt; &lt;strong&gt;API 密钥&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制密钥&lt;/p&gt;
&lt;p&gt;将你的 &lt;strong&gt;发布密钥&lt;/strong&gt; 和 &lt;strong&gt;秘密密钥&lt;/strong&gt; (用于开发) 复制到 &lt;code&gt;.env.local&lt;/code&gt; 文件中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Stripe 密钥
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;h3&gt;步骤 5：设置 Stripe Webhook 端点&lt;/h3&gt;
&lt;p&gt;&amp;lt;Tabs&amp;gt;
&amp;lt;TabItem label=&quot;Local Dev&quot;&amp;gt;
由于本地开发，Stripe 的 Webhook 无法直接访问，因此你需要使用 Stripe CLI 来监听 Webhook。打开终端并运行本地监听器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
&amp;gt; stripe listen --forward-to localhost:3000/api/webhooks/stripe
A newer version of the Stripe CLI is available, please update to: v1.28.0
&amp;gt; Ready! You are using Stripe API Version [2022-08-01]. Your webhook signing secret is whsec_xxxxx (^C to quit)
```

你将在终端中看到 Webhook 签名密钥。将其复制到 *.env.local* 文件中。

```bash collapse={2-3} ins={4}
# 从 Stripe 仪表板获取: https://dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;TabItem label=&quot;Production&quot;&amp;gt;
打开 Stripe 仪表板并导航到 &lt;strong&gt;开发者&lt;/strong&gt; &amp;gt; &lt;strong&gt;Webhook&lt;/strong&gt;，创建一个新的 Webhook。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**API 版本**: 2025.06-30.basil

**事件**: 确保你选择了以下事件。

&amp;lt;FileTree&amp;gt;
  - Checkout
    - checkout.session.completed
  - Payment&amp;amp;Intent
    - payment_intent.succeeded
    - payment_intent.payment_failed
  - Product
    - product.created
    - product.updated
    - product.deleted
  - Price
    - price.created
    - price.updated
    - price.deleted
&amp;lt;/FileTree&amp;gt;

完成设置步骤。打开 Webhook URL 并复制签名密钥 **whsec_xxxxx** 到 *.env.local* 文件中。

```bash collapse={2-3} ins={4}
# 从 Stripe 仪表板获取: https://dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/TabItem&amp;gt;
&amp;lt;/Tabs&amp;gt;&lt;/p&gt;
&lt;h3&gt;步骤 6：初始化 Supabase&lt;/h3&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;登录 Supabase&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase login
你好，Supabase！按 Enter 打开浏览器并自动登录。

这是你的登录链接，如果浏览器没有打开 https://supabase.com/dashboard/cli/login?session_id=uuid&amp;amp;token_name=cli_@hostname&amp;amp;public_key=string

Enter your verification code: c4d28d97
Token cli_@hostname created successfully.

You are now logged in. Happy coding!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化 Supabase&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase init
Generate VS Code settings for Deno? [y/N] N
Generate IntelliJ Settings for Deno? [y/N] N
Finished supabase init.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链接到你的 Supabase 项目&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase link --project-ref your-project-id
输入你的数据库密码 (或留空跳过): // 输入你的数据库密码
Connecting to remote database...
NOTICE (42P06): schema &quot;supabase_migrations&quot; already exists, skipping
Finished supabase link.
WARNING: Local config differs from linked project. Try updating supabase/config.toml
diff supabase/config.toml your-project-id
--- supabase/config.toml
+++ your-project-id
@@ -54,8 +54,8 @@

[auth]
enabled = true
-site_url = &quot;http://127.0.0.1:3000&quot;
-additional_redirect_urls = [&quot;https://127.0.0.1:3000&quot;]
+site_url = &quot;http://localhost:3000&quot;
+additional_redirect_urls = []
jwt_expiry = 3600
enable_refresh_token_rotation = true
refresh_token_reuse_interval = 10
@@ -96,9 +96,9 @@
[auth.email]
enable_signup = true
double_confirm_changes = true
-enable_confirmations = false
+enable_confirmations = true
secure_password_change = false
-max_frequency = &quot;1s&quot;
+max_frequency = &quot;1m0s&quot;
otp_length = 6
otp_expiry = 3600
[auth.email.template]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用迁移&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npx supabase db push
Connecting to remote database...
你想将这些迁移推送到远程数据库吗？
• 20250710080949_init.sql

[Y/n] Y
NOTICE (42P06): schema &quot;supabase_migrations&quot; already exists, skipping
NOTICE (42P07): relation &quot;schema_migrations&quot; already exists, skipping
NOTICE (42701): column &quot;statements&quot; of relation &quot;schema_migrations&quot; already exists, skipping
NOTICE (42701): column &quot;name&quot; of relation &quot;schema_migrations&quot; already exists, skipping
Applying migration 20250710080949_init.sql...
NOTICE (42710): extension &quot;uuid-ossp&quot; already exists, skipping
Finished supabase db push.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;h3&gt;步骤 7：初始化 Stripe 产品&lt;/h3&gt;
&lt;p&gt;运行自动化脚本来创建产品和服务在你的 Stripe 账户中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npm run stripe:init

# 你将看到以下输出。

&amp;gt; nextjs-stripe-app@0.1.0 stripe:init
&amp;gt; ts-node scripts/stripe-init.ts

(node:76080) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:76080) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///Users/obiscr/projects/nextjs-stripe-supabase-starter/scripts/stripe-init.ts is not specified and it doesn&apos;t parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add &quot;type&quot;: &quot;module&quot; to /Users/obiscr/projects/nextjs-stripe-supabase-starter/package.json.
[dotenv@17.2.0] injecting env (7) from .env.local (tip: ⚙️  load multiple .env files with { path: [&apos;.env.local&apos;, &apos;.env&apos;] })
🚀 Starting Stripe initialization...


📋 Processing product: Premium Membership
──────────────────────────────────────────────────
📦 Creating product: Premium Membership
✅ Product created: ************** - Premium Membership
💰 Creating price: Premium Monthly - $29.99
✅ Price created: ************** - Premium Monthly
💰 Creating price: Premium Yearly - $99.99
✅ Price created: ************** - Premium Yearly
💰 Creating price: Premium Lifetime - $199.99
✅ Price created: ************** - Premium Lifetime
✨ Completed Premium Membership with 3 prices

📋 Processing product: Cloud Storage
──────────────────────────────────────────────────
📦 Creating product: Cloud Storage
✅ Product created: ************** - Cloud Storage
💰 Creating price: 50GB Storage - $9.99
✅ Price created: ************** - 50GB Storage
💰 Creating price: 100GB Storage - $19.99
✅ Price created: ************** - 100GB Storage
💰 Creating price: 500GB Storage - $49.99
✅ Price created: ************** - 500GB Storage
✨ Completed Cloud Storage with 3 prices

📋 Processing product: API Access
──────────────────────────────────────────────────
📦 Creating product: API Access
✅ Product created: ************** - API Access
💰 Creating price: Basic API - $19.99
✅ Price created: ************** - Basic API
💰 Creating price: Pro API - $49.99
✅ Price created: ************** - Pro API
💰 Creating price: Enterprise API - $99.99
✅ Price created: ************** - Enterprise API
✨ Completed API Access with 3 prices

🎉 Stripe initialization completed successfully!

📊 Summary:
────────────────────────────────────────────────────────────

🏷️  Premium Membership (**************)
   💰 Premium Monthly: ************** - $29.99
   💰 Premium Yearly: ************** - $99.99
   💰 Premium Lifetime: ************** - $199.99

🏷️  Cloud Storage (**************)
   💰 50GB Storage: ************** - $9.99
   💰 100GB Storage: ************** - $19.99
   💰 500GB Storage: ************** - $49.99

🏷️  API Access (**************)
   💰 Basic API: ************** - $19.99
   💰 Pro API: ************** - $49.99
   💰 Enterprise API: ************** - $99.99

✅ You can now run your application and see the products!
🔗 Stripe Dashboard: https://dashboard.stripe.com/products
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建配置中定义的所有产品和价格。你可以在 Stripe 仪表板中查看它们。&lt;/p&gt;
&lt;p&gt;此时，你可以在 Stripe Webhook 仪表板中看到 Webhook 触发器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A newer version of the Stripe CLI is available, please update to: v1.28.0
&amp;gt; Ready! You are using Stripe API Version [2022-08-01]. Your webhook signing secret is whsec_xxxxx (^C to quit)
2025-07-10 21:44:51   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:52   --&amp;gt; plan.created [evt_******************]
2025-07-10 21:44:52  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:52   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:53  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:53   --&amp;gt; plan.created [evt_******************]
2025-07-10 21:44:53  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:53   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:54   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:54   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:54  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:55  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:55   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:56  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:56   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:57   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:57  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58   --&amp;gt; product.created [evt_******************]
2025-07-10 21:44:58  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:44:58   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:59   --&amp;gt; price.created [evt_******************]
2025-07-10 21:44:59  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:45:00   --&amp;gt; price.created [evt_******************]
2025-07-10 21:45:00  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
2025-07-10 21:45:01  &amp;lt;--  [200] POST http://localhost:3000/api/webhooks/stripe [evt_******************]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything goes well, you will see the product and product item data in supabase.&lt;/p&gt;
&lt;h3&gt;步骤 8：生成数据类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; npm run supabase:gen-types

&amp;gt; nextjs-stripe-app@0.1.0 supabase:gen-types
&amp;gt; supabase gen types typescript --linked &amp;gt; lib/database.types.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 9：启动开发服务器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中打开 &lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;完成完整的购买流程&lt;/h2&gt;
&lt;p&gt;&amp;lt;Steps&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;注册新用户&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击 &quot;登录&quot; 按钮,&lt;/li&gt;
&lt;li&gt;切换到 &quot;注册&quot; 标签&lt;/li&gt;
&lt;li&gt;使用电子邮件和密码创建一个新帐户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/nextjs-stripe-supabase-starter-login.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览产品&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看 3 个产品类别&lt;/li&gt;
&lt;li&gt;每个产品有多个定价层&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/nextjs-stripe-supabase-starter-items.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行测试购买&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击任何产品上的 &quot;立即购买&quot;&lt;/li&gt;
&lt;li&gt;你将被重定向到 Stripe Checkout&lt;/li&gt;
&lt;li&gt;使用测试卡号: &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用任何未来的到期日期和 CVC&lt;/li&gt;
&lt;li&gt;完成支付&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/nextjs-stripe-supabase-starter-buy.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支付成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你将被重定向到成功页面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/nextjs-stripe-supabase-starter-finish.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;验证购买&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回主页&lt;/li&gt;
&lt;li&gt;购买的商品应显示 &quot;已购买&quot; 状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/nextjs-stripe-supabase-starter-purchased.png&quot; alt=&quot;&quot; /&gt;
&amp;lt;/Steps&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这是一个使用 Stripe 和 Supabase 的简单示例。你可以使用它作为你自己的项目的起点。&lt;/p&gt;
&lt;p&gt;你可以在以下位置找到源代码：&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://github.com/obiscr/nextjs-stripe-supabase-starter.git&quot;
title=&quot;nextjs-stripe-supabase-starter&quot;
description=&quot;一个基于Next.js、Stripe Checkout和Supabase构建的完整电子商务演示系统，包含用户认证、产品管理、安全支付处理及自动购买跟踪功能。&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://nextjs-stripe-supabase-starter.vercel.app/&quot;
title=&quot;在线案例&quot;
description=&quot;Next.js、Stripe 和 Supabase 启动模板的实时演示&quot;
/&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>使用 ProGuard 混淆你的插件</title><link>https://obiscr.com/zh-cn/blog/obfuscate-plugin-with-proguard</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/obfuscate-plugin-with-proguard</guid><description>本文将向你展示一个逐步演示，使用 ProGuard 混淆你的插件，从真实示例开始。</description><content:encoded>&lt;p&gt;import { Aside } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;插件混淆已经在官方文档中详细描述，请参阅：&lt;a href=&quot;https://plugins.jetbrains.com/docs/marketplace/obfuscate-the-plugin.html&quot;&gt;obfuscate the plugin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;所以，本文将向你展示一个逐步演示，使用 ProGuard 混淆你的插件，从真实示例开始。&lt;/p&gt;
&lt;p&gt;要查看比较文件，ProGuard 混淆的关键代码已添加到此 &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/commit/a55a6b643351456a67dd036ca2496a73753519a1&quot;&gt;commit&lt;/a&gt;，所有代码都在此 &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate&quot;&gt;repository&lt;/a&gt; 中。&lt;/p&gt;
&lt;h2&gt;添加 ProGuard&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;build.gradle.kts&lt;/strong&gt; 中，添加 &lt;code&gt;buildscript&lt;/code&gt; 节点以导入 &lt;code&gt;proguard&lt;/code&gt; 依赖项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buildscript {
    repositories {
        maven {
            setUrl(&quot;https://maven.aliyun.com/repository/public/&quot;)
            setUrl(&quot;https://maven.aliyun.com/nexus/content/groups/public/&quot;)
            setUrl(&quot;https://plugins.gradle.org/m2/&quot;)
            setUrl(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;)
        }
        mavenCentral()
        gradlePluginPortal()
    }

    dependencies {
        classpath(&quot;com.guardsquare:proguard-gradle:7.3.2&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 ProGuard&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;build.gradle.kts&lt;/strong&gt; 中注册一个新 &lt;code&gt;task&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    // Register a new task, task name is &quot;proguard&quot;
    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        dependsOn(instrumentedJar)
        verbose()

        val javaHome = System.getProperty(&quot;java.home&quot;)
        File(&quot;$javaHome/jmods/&quot;).listFiles()!!.forEach { libraryjars(it.absolutePath)}

        // Use the jar task output as a input jar. This will automatically add the necessary task dependency.
        injars(&quot;build/libs/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;)
        outjars(&quot;build/obfuscated/output/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;)


        libraryjars(configurations.compileClasspath.get())

        dontshrink()
        dontoptimize()

        adaptclassstrings(&quot;**.xml&quot;)
        adaptresourcefilecontents(&quot;**.xml&quot;)

        // Allow methods with the same signature, except for the return type,
        // to get the same obfuscation name.
        overloadaggressively()

        // Put all obfuscated classes into the nameless root package.
        repackageclasses(&quot;&quot;)
        dontwarn()

        printmapping(&quot;build/obfuscated/output/${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}-ProGuard-ChangeLog.txt&quot;)

        target(properties(&quot;pluginVersion&quot;))

        adaptresourcefilenames()
        optimizationpasses(9)
        allowaccessmodification()

        keepattributes(&quot;Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod&quot;)

        keep(&quot;&quot;&quot;
            class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
             &quot;&quot;&quot;.trimIndent()
        )

        keepclassmembers(&quot;&quot;&quot;
            class * {public static ** INSTANCE;}
             &quot;&quot;&quot;.trimIndent()
        )
        keep(&quot;class com.intellij.util.* {*;}&quot;)
    }


    prepareSandbox {
        if (properties(&quot;enableProGuard&quot;).toBoolean()) {
            dependsOn(&quot;proguard&quot;)
            pluginJar.set(File(&quot;build/obfuscated/output/instrumented-${properties(&quot;pluginName&quot;)}-${properties(&quot;pluginVersion&quot;)}.jar&quot;))
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
这些是 ProGuard 的语法，更多规则请参阅：&lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;Official ProGuard Documentation&lt;/a&gt;，我们重点关注最后两个 &lt;code&gt;keep&lt;/code&gt; 和 &lt;code&gt;keepclassmembers&lt;/code&gt;。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;构建测试&lt;/h2&gt;
&lt;p&gt;配置完成后，运行插件，调试消息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;21:40:12: Executing &apos;runIde&apos;...

Starting Gradle Daemon...
Connected to the target VM, address: &apos;127.0.0.1:12563&apos;, transport: &apos;socket&apos;
Gradle Daemon started in 915 ms
&amp;gt; Task :initializeIntelliJPlugin
&amp;gt; Task :patchPluginXml UP-TO-DATE

&amp;gt; Task :verifyPluginConfiguration
[gradle-intellij-plugin :verifyPluginConfiguration] The following plugin configuration issues were found:
- The Java configuration specifies sourceCompatibility=11 but IntelliJ Platform 2022.3.3 requires sourceCompatibility=17.
See: https://jb.gg/intellij-platform-versions

&amp;gt; Task :compileKotlin NO-SOURCE
&amp;gt; Task :compileJava UP-TO-DATE
&amp;gt; Task :processResources UP-TO-DATE
&amp;gt; Task :classes UP-TO-DATE
&amp;gt; Task :setupInstrumentCode
&amp;gt; Task :instrumentCode UP-TO-DATE
&amp;gt; Task :jar UP-TO-DATE
&amp;gt; Task :inspectClassesForKotlinIC UP-TO-DATE
&amp;gt; Task :instrumentedJar UP-TO-DATE
&amp;gt; Task :proguard &amp;lt;=== proguard task 已经成功执行。
&amp;gt; Task :prepareSandbox
Disconnected from the target VM, address: &apos;127.0.0.1:12563&apos;, transport: &apos;socket&apos;
Connected to the target VM, address: &apos;localhost:12616&apos;, transport: &apos;socket&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;混淆结果&lt;/h2&gt;
&lt;p&gt;混淆后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/obfucated.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;左侧是文件名已混淆，右侧是文件类名、方法名和变量名已混淆。&lt;/p&gt;
&lt;p&gt;&amp;lt;Aside type=&quot;note&quot; title=&quot;Note&quot;&amp;gt;
这只是非常基本的混淆效果，如果你需要更复杂的混淆，可以参考上面提到的 &lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;ProGuard 官方文档&lt;/a&gt;。
&amp;lt;/Aside&amp;gt;&lt;/p&gt;
&lt;h2&gt;特殊情况&lt;/h2&gt;
&lt;p&gt;在 IntelliJ 插件项目中，有一些文件不需要混淆。本演示中列出了两个案例。&lt;/p&gt;
&lt;h3&gt;通过反射调用&lt;/h3&gt;
&lt;p&gt;项目中的一个文件是 &lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/blob/main/src/main/java/com/obiscr/template/MyDefaultFileType.java&quot;&gt;MyDefaultFileType.java&lt;/a&gt;。这个文件添加了一个新的文件类型，我们称之为 J-file，具有 &lt;code&gt;*.j&lt;/code&gt; 或 &lt;code&gt;*.J&lt;/code&gt; 文件扩展名。&lt;/p&gt;
&lt;p&gt;这个文件已经在 plugin.xml 中注册&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;extensions defaultExtensionNs=&quot;com.intellij&quot;&amp;gt;
    ...
    &amp;lt;fileType name=&quot;MyNativeFile&quot; implementationClass=&quot;com.obiscr.template.MyDefaultFileType&quot; fieldName=&quot;INSTANCE&quot;
              extensions=&quot;j;J&quot; order=&quot;first&quot;/&amp;gt;
    ...
&amp;lt;/extensions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点是 fieldName 属性：&lt;strong&gt;INSTANCE&lt;/strong&gt;，它对应于 &lt;code&gt;MyDefaultFileType.java&lt;/code&gt; 中的 &lt;strong&gt;INSTANCE&lt;/strong&gt;。换句话说，它们必须相同。否则，当反射调用时，属性将找不到。&lt;/p&gt;
&lt;p&gt;例如，一旦我们删除了 proguard task 的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        ...

        // keepclassmembers(&quot;&quot;&quot;
        //     class * {public static ** INSTANCE;}
        //      &quot;&quot;&quot;.trimIndent()
        // )
        ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/INSTANCE-obfucated.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;运行插件并获取错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2023-09-24 21:31:18,647 [   3018]   WARN - #c.i.e.RunManager - Must be not called before project components initialized
Info  | RdCoroutineScope          | 53:DefaultDispatcher-worker-35 | RdCoroutineHost overridden
2023-09-24 21:31:21,580 [   5951] SEVERE - #c.i.o.f.i.FileTypeManagerImpl - INSTANCE
java.lang.NoSuchFieldException: INSTANCE
    at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
    at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.instantiateFileTypeBean(FileTypeManagerImpl.java:492)
    at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.mergeOrInstantiateFileTypeBean(FileTypeManagerImpl.java:463)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表明 &lt;strong&gt;INSTANCE&lt;/strong&gt; 属性找不到。因此，对于这种情况，你可以使用 proguard 的 keepclassmembers 来保持 &lt;strong&gt;INSTANCE&lt;/strong&gt; 属性在所有类中不被混淆。当然，如果你愿意，你也可以保持 INSTANCE 在所有实现 INativeFileType 的类中不被混淆。这是一种更精确的控制。&lt;/p&gt;
&lt;p&gt;你可能想知道，既然这个问题可以通过在 plugin.xml 中将 fieldName 与 &lt;code&gt;MyDefaultFileType.java&lt;/code&gt; 中的 &lt;strong&gt;INSTANCE&lt;/strong&gt; 保持相同来避免，为什么不通过将它改为相同的值来避免这个问题？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/INSTANCE-keep-it.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;是的，这在原则上是可以的。然而，混淆每次都会生成一个随机变量名。也就是说，属性 fieldName 必须首先被指定为某个确切的值，然后开始打包，在打包后的类文件中，INSTANCE 不一定是上面指定的值。因此，我们仍然需要从混淆规则中排除 &lt;strong&gt;INSTANCE&lt;/strong&gt; 以避免这个问题。&lt;/p&gt;
&lt;h3&gt;本地数据存储&lt;/h3&gt;
&lt;p&gt;在许多情况下，需要存储一些本地数据，例如设置数据、环境数据等。在这种情况下，你需要使用 @State 来指定一个文件来存储数据。在这种情况下，你需要使用 &lt;code&gt;@State&lt;/code&gt; 来指定存储文件。例如，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.obiscr.template;

import com.intellij.openapi.components.*;
import com.intellij.openapi.project.Project;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@State( name = &quot;com.obiscr.template.MyState&quot;, storages = @Storage(&quot;my-state/state.xml&quot;))
public class MyState implements PersistentStateComponent&amp;lt;MyState&amp;gt; {
    public String currentVersion = &quot;&quot;;
    public Boolean enableFeature = true;
    public String myCustomKey = &quot;&quot;;

    public static MyState getInstance(@NotNull Project project) {
        return project.getService(MyState.class);
    }

    @Nullable
    @Override
    public MyState getState() {
        return this;
    }

    @Override
    public void loadState(@NotNull MyState state) {
        XmlSerializerUtil.copyBean(state, this);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里定义了一个存储文件和一些属性，它将在项目 .idea 目录的 my-state 目录中创建一个新 &lt;code&gt;state.xml&lt;/code&gt; 文件，内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project version=&quot;4&quot;&amp;gt;
  &amp;lt;component name=&quot;com.obiscr.template.MyState&quot;&amp;gt;
    &amp;lt;option name=&quot;currentVersion&quot; value=&quot;v1.0.1&quot; /&amp;gt;
    &amp;lt;option name=&quot;enableFeature&quot; value=&quot;false&quot; /&amp;gt;
    &amp;lt;option name=&quot;myCustomKey&quot; value=&quot;value&quot; /&amp;gt;
  &amp;lt;/component&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样，这里的 option 中的 name 属性对应于 &lt;code&gt;MyState.java&lt;/code&gt; 中的三个属性。这些属性不应该被混淆；如果它们被混淆，原始数据将不会被读取。例如，如果你设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;currentVersion： v1.0.1&lt;/li&gt;
&lt;li&gt;enableFeature：false&lt;/li&gt;
&lt;li&gt;myCustomKey：value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;混淆后，假设 MyState.java 中有以下属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;currentVersion -&amp;gt; a&lt;/li&gt;
&lt;li&gt;enableFeature -&amp;gt; d&lt;/li&gt;
&lt;li&gt;myCustomKey -&amp;gt; c&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;state.xml 中的数据无法读取。因为 option 中没有 name 是：a, d, c 的数据，所以最终读取出的值是 MyState.java 中定义的默认值，一旦这个值发生变化，将在 xml 中再次添加一个新属性。name 是混淆后的变量名。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现在 currentVersion 已经被混淆为 &lt;code&gt;a&lt;/code&gt;，所以如果 currentVersion 的值发生变化，将其更改为 &lt;code&gt;v1.0.2&lt;/code&gt;，那么 &lt;code&gt;state.xml&lt;/code&gt; 将如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project version=&quot;4&quot;&amp;gt;
  &amp;lt;component name=&quot;com.obiscr.template.MyState&quot;&amp;gt;
    &amp;lt;option name=&quot;currentVersion&quot; value=&quot;v1.0.1&quot; /&amp;gt;
    &amp;lt;option name=&quot;enableFeature&quot; value=&quot;false&quot; /&amp;gt;
    &amp;lt;option name=&quot;myCustomKey&quot; value=&quot;value&quot; /&amp;gt;
    &amp;lt;!-- Added --&amp;gt;
    &amp;lt;option name=&quot;a&quot; value=&quot;v1.0.2&quot; /&amp;gt;
  &amp;lt;/component&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将添加一个 &lt;code&gt;&amp;lt;option name=&quot;a&quot; value=&quot;v1.0.2&quot; /&amp;gt;&lt;/code&gt;。因此，只要混淆后的变量名不同，它就会继续添加。所以这里有一个解决方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    register&amp;lt;proguard.gradle.ProGuardTask&amp;gt;(&quot;proguard&quot;) {
        ...

        keep(&quot;&quot;&quot;
            class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
             &quot;&quot;&quot;.trimIndent()
        )
        ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个规则忽略所有实现 PersistentStateComponent 的类。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文简要介绍了如何使用 ProGuard 混淆 IntlliJ 插件并将其集成到 gradle 任务中。&lt;/p&gt;
&lt;p&gt;实际上，我更喜欢称之为一个框架。具体的混淆规则需要根据你自己的项目进行定制。我希望它能帮助你。&lt;/p&gt;
&lt;h2&gt;免责声明&lt;/h2&gt;
&lt;p&gt;这只是一个研究项目，请根据你自己的项目使用。&lt;/p&gt;
&lt;p&gt;我不会对使用这种混淆方案造成的任何损害负责。&lt;/p&gt;
&lt;h2&gt;有用的链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate&quot;&gt;Code Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/obiscr/intellij-plugin-proguard-obfuscate/commit/a55a6b643351456a67dd036ca2496a73753519a1&quot;&gt;Key commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.guardsquare.com/manual/setup/gradle&quot;&gt;ProGuard documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/marketplace/obfuscate-the-plugin.html&quot;&gt;JetBrains Obfuscate the plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/persistence.html&quot;&gt;Persistence data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plugins.jetbrains.com/docs/intellij/language-and-filetype.html&quot;&gt;Add new file type&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ResoLine App - 你的专业屏幕分辨率助手工具</title><link>https://obiscr.com/zh-cn/blog/resoline-v1-0-1</link><guid isPermaLink="true">https://obiscr.com/zh-cn/blog/resoline-v1-0-1</guid><description>ResoLine 是一个专业的屏幕分辨率助手工具，帮助你直观地预览和比较屏幕上不同分辨率的实际显示效果。</description><content:encoded>&lt;p&gt;import { LinkCard } from &apos;@astrojs/starlight/components&apos;;&lt;/p&gt;
&lt;p&gt;ResoLine 是一个专业的屏幕分辨率助手工具，帮助你直观地预览和比较屏幕上不同分辨率的实际显示效果。&lt;/p&gt;
&lt;h2&gt;安装 ResoLine&lt;/h2&gt;
&lt;p&gt;&amp;lt;LinkCard
href=&quot;https://apps.apple.com/cn/app/resoline/id6744457578&quot;
title=&quot;App Store&quot;
description=&quot;Download from App Store&quot;
/&amp;gt;&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;h3&gt;Localization&lt;/h3&gt;
&lt;p&gt;目前支持简体中文和英文。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/appearance.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../../../assets/blog/images/preference.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;自定义外观&lt;/h3&gt;
&lt;p&gt;你可以自定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于线条：线条类型 / 颜色 / 厚度。&lt;/li&gt;
&lt;li&gt;对于预览文本：字体颜色 / 背景颜色 / 透明度。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;自定义分辨率&lt;/h3&gt;
&lt;p&gt;系统已经有一些常见的分辨率内置，你也可以根据需要添加自定义分辨率。&lt;/p&gt;
</content:encoded></item></channel></rss>