<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SIMULAR Lab]]></title><description><![CDATA[SIMULAR Lab]]></description><link>https://lab.simular.co</link><generator>RSS for Node</generator><lastBuildDate>Sat, 09 May 2026 23:02:40 GMT</lastBuildDate><atom:link href="https://lab.simular.co/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Concurrently Run Hookdeck (Or any process) with Nuxt / Vite App]]></title><description><![CDATA[Hookdeck is a convenient and easy-to-use webhook infrastructure that can also be easily used for local development as a replacement for ngrok. However, during local development, if you need to frequently receive hooks, you must start two Terminal ses...]]></description><link>https://lab.simular.co/concurrently-run-hookdeck-with-nuxt-vite-app</link><guid isPermaLink="true">https://lab.simular.co/concurrently-run-hookdeck-with-nuxt-vite-app</guid><category><![CDATA[hookdeck]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[vite]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[Simon Asika]]></dc:creator><pubDate>Sat, 15 Nov 2025 08:14:32 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://hookdeck.com/">Hookdeck</a> is a convenient and easy-to-use webhook infrastructure that can also be easily used for local development as a replacement for ngrok. However, during local development, if you need to frequently receive hooks, you must start two Terminal sessions to run both the Nuxt/Vite App and Hookdeck separately.</p>
<p>In one terminal:</p>
<pre><code class="lang-xml">nuxt dev
</code></pre>
<p>Another terminal:</p>
<pre><code class="lang-xml">
hookdeck listen 3000 {source}
</code></pre>
<p>The downside of this is that when port 3000 is occupied, Nuxt will fall back to 3001, 3002, and so on, using different ports. This can easily confuse engineers who need to frequently switch environments about which Hookdeck tunnel they are currently using.</p>
<h2 id="heading-solutions-1-use-concurrently">Solutions 1: Use “concurrently”</h2>
<p>NPM has a package called <a target="_blank" href="https://www.npmjs.com/package/concurrently">concurrently</a> that allows us to run nuxt dev and hookdeck in parallel with a single command. When you stop the process, both threads will terminate at the same time, preventing confusion with multiple terminals.</p>
<p>Installation:</p>
<pre><code class="lang-bash">npm install concurrently
</code></pre>
<p>Change the dev command (if using Vite, use <code>vite dev</code>)</p>
<pre><code class="lang-diff">  "scripts": {
    ...
<span class="hljs-deletion">-    "dev": "nuxt dev",</span>
<span class="hljs-addition">+    "dev": "concurrently --names hook,nuxt 'hookdeck listen 3000 xxx' 'nuxt dev'",</span>
</code></pre>
<p>Run:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p>This way, you can run two processes in parallel.</p>
<pre><code class="lang-bash">[hook] Dashboard
[hook] 👉 Inspect and replay events: https://dashboard.hookdeck.com?team_id=xxxxxxxxxxxx
[hook] 
[hook] Sources
[hook] 🔌 xxx URL: https://hkdk.events/xxxxxxxxxxxx
[hook] 
[hook] Connections
[hook] xxx -&gt; xxx forwarding to /api/.../
[hook] 
[hook] Getting ready...
[nuxt] [nuxi] Nuxt 3.20.1 (with Nitro 2.12.9, Vite 7.2.2 and Vue 3.5.24)
[nuxt] 
[nuxt]   ➜ Local:    http://localhost:3000/
[nuxt]   ➜ Network:  use --host to expose
[nuxt] 
[nuxt]   ➜ DevTools: press Shift + Option + D <span class="hljs-keyword">in</span> the browser (v3.1.0)
</code></pre>
<p>However, the downside of this approach is that we still can't make Hookdeck automatically detect the Nuxt dev server's port. We can only use port 3000 by default. A compromise is to specify the port each time we run it, so we tried modifying the command.</p>
<pre><code class="lang-bash"><span class="hljs-string">"dev"</span>: <span class="hljs-string">"concurrently --names hook,nuxt 'hookdeck listen <span class="hljs-variable">${PORT:-3000}</span> rankwing' 'nuxt dev --port <span class="hljs-variable">${PORT:-3000}</span>'"</span>
</code></pre>
<p>Then you can customize the port when running.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Use default 3000 port</span>
npm run dev

<span class="hljs-comment"># Use 3001 port</span>
PORT=3001 npm run dev
</code></pre>
<h2 id="heading-solutions-2-custom-vite-plugin">Solutions 2: Custom Vite Plugin</h2>
<p>The main drawback of the above solution is that it still can't automatically detect the Nuxt/Vite port. We tried using a Vite plugin to automatically use the current port when the Nuxt/Vite dev server starts.</p>
<p>What this plugin does is quite simple. It checks if there is a <code>hookdeck.xxxx.pid</code> stored in a certain tmp directory. If there is a pid, then no matter how many times the Nuxt dev server restarts, it won't repeatedly create a hookdeck process, preventing memory leaks or Zombie processes.</p>
<p>Note that this <code>hookdeck.xxxx.pid</code> must be stored in a tmp directory. If you are using a simple Vite project, please create a tmp directory to store it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// plugins/hookdeck.ts</span>
<span class="hljs-keyword">import</span> { spawn } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:child_process'</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> Plugin } <span class="hljs-keyword">from</span> <span class="hljs-string">'vite'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> HookdeckOptions {
  hookdeckBinary?: <span class="hljs-built_in">string</span>;
  source?: <span class="hljs-built_in">string</span>;
  port?: <span class="hljs-built_in">number</span>;
  pidFile?: <span class="hljs-built_in">string</span>;
  urlMap?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;;
  enabled?: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hookdeck</span>(<span class="hljs-params">options: HookdeckOptions = {}</span>): <span class="hljs-title">Plugin</span> </span>{
  <span class="hljs-keyword">return</span> {
    name: <span class="hljs-string">'hookdeck-concurrently'</span>,
    <span class="hljs-keyword">async</span> configureServer(server) {
      <span class="hljs-keyword">const</span> enabled = options.enabled ?? process.env.NUXT_HOOKDECK_ENABLED === <span class="hljs-string">'1'</span>;

      <span class="hljs-keyword">if</span> (!enabled) {
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> port = (server.httpServer?.address() <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>)?.port ?? options.port ?? <span class="hljs-number">3000</span>;
      <span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> In vite, you must create a tmp folder and exclude it by .gitignore</span>
      <span class="hljs-keyword">const</span> pidFile = options.pidFile ?? <span class="hljs-string">`./.nuxt/hookdeck.<span class="hljs-subst">${port}</span>.pid`</span>;
      <span class="hljs-keyword">const</span> source = options.source || <span class="hljs-string">''</span>;
      <span class="hljs-keyword">const</span> hookdeckBinary = options.hookdeckBinary || <span class="hljs-string">'hookdeck'</span>;

      <span class="hljs-keyword">if</span> (!source) {
        <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'[vite hookdeck] No source provided, skipping hookdeck setup.'</span>);
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (fs.existsSync(pidFile)) {
        <span class="hljs-keyword">const</span> oldPid = fs.readFileSync(pidFile, { encoding: <span class="hljs-string">'utf-8'</span> });

        <span class="hljs-keyword">try</span> {
          process.kill(<span class="hljs-built_in">Number</span>(oldPid), <span class="hljs-number">0</span>);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[vite hookdeck] Hookdeck is already running with PID: <span class="hljs-subst">${oldPid}</span>`</span>);
          <span class="hljs-keyword">return</span>;
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[vite hookdeck] Old hookdeck process not found, starting a new one...`</span>);
        }
      }

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[vite hookdeck] Starting hookdeck on port <span class="hljs-subst">${port}</span>`</span>)

      <span class="hljs-keyword">const</span> child = spawn(hookdeckBinary, [<span class="hljs-string">'listen'</span>, <span class="hljs-built_in">String</span>(port), source], {
        shell: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Windows must add this</span>
      });

      child.stdout.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">data</span>) </span>{
        <span class="hljs-built_in">console</span>.log(data.toString().trim());
        <span class="hljs-keyword">const</span> urlMap = options.urlMap;
        <span class="hljs-keyword">const</span> matchProxy = data.toString().match(<span class="hljs-regexp">/URL: https:\/\/(hkdk\.events\/([a-zA-Z0-9]+))/</span>);
        <span class="hljs-keyword">const</span> matchDest = data.toString().match(<span class="hljs-regexp">/forwarding to ([a-zA-Z0-9_\-\/\:]+)/</span>);

        <span class="hljs-keyword">if</span> (urlMap &amp;&amp; matchProxy &amp;&amp; matchDest) {
          <span class="hljs-keyword">const</span> url = matchProxy[<span class="hljs-number">1</span>];
          <span class="hljs-keyword">const</span> path = matchDest[<span class="hljs-number">1</span>];
          <span class="hljs-keyword">const</span> data: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;[] = [];

          <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [key, value] <span class="hljs-keyword">of</span> <span class="hljs-built_in">Object</span>.entries(urlMap)) {
            <span class="hljs-keyword">const</span> endpoint = <span class="hljs-string">'https://'</span> + <span class="hljs-string">`<span class="hljs-subst">${url}</span>/<span class="hljs-subst">${value}</span>`</span>.replace(<span class="hljs-regexp">/\/\//g</span>, <span class="hljs-string">'/'</span>);
            <span class="hljs-keyword">const</span> dest = <span class="hljs-string">'http://'</span> + <span class="hljs-string">`localhost:<span class="hljs-subst">${port}</span><span class="hljs-subst">${path}</span>/<span class="hljs-subst">${value}</span>`</span>.replace(<span class="hljs-regexp">/\/\//g</span>, <span class="hljs-string">'/'</span>);

            <span class="hljs-comment">// console.log(`  [${key}] ${endpoint} =&gt; ${dest}`);</span>
            data.push({
              Name: key,
              Hook: endpoint,
              Dest: dest
            });
          }

          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">''</span>);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'HOOKDECK URL MAPPING:'</span>);
          <span class="hljs-built_in">console</span>.table(data);
        }
      });

      fs.writeFileSync(pidFile, child.pid?.toString() || <span class="hljs-string">''</span>, { encoding: <span class="hljs-string">'utf-8'</span> });

      child.on(<span class="hljs-string">'error'</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'[vite hookdeck error]'</span>, err)
      })

      <span class="hljs-keyword">let</span> exit = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[vite hookdeck] Shutting down hookdeck...'</span>)
        child?.kill(<span class="hljs-string">'SIGTERM'</span>)
        fs.unlinkSync(pidFile);
      };

      server.httpServer?.on(<span class="hljs-string">'close'</span>, exit);

      process.on(<span class="hljs-string">'SIGINT'</span>, exit);
      process.on(<span class="hljs-string">'SIGTERM'</span>, exit);
      process.on(<span class="hljs-string">'exit'</span>, exit);
    }
  }
};
</code></pre>
<p>Register in <code>nuxt.config.ts</code> :</p>
<pre><code class="lang-typescript">  vite: {
    plugins: [
      hookdeck({
        source: <span class="hljs-keyword">import</span>.meta.env.NUXT_HOOKDECK_SOURCE || <span class="hljs-string">'{your hookdeck source}'</span>,
        urlMap: {
          Clerk: <span class="hljs-string">'hook/clerk'</span>,
          Paddle: <span class="hljs-string">'hook/paddle'</span>,
        },
        enabled: <span class="hljs-keyword">import</span>.meta.env.NUXT_HOOKDECK_ENABLED === <span class="hljs-string">'1'</span>,
      }),
    ],
</code></pre>
<p>Register in <code>vite.config.ts</code></p>
<pre><code class="lang-typescript">    plugins: [
      hookdeck({
        source: <span class="hljs-keyword">import</span>.meta.env.NUXT_HOOKDECK_SOURCE || <span class="hljs-string">'{your hookdeck source}'</span>,
        urlMap: {
          Clerk: <span class="hljs-string">'hook/clerk'</span>,
          Paddle: <span class="hljs-string">'hook/paddle'</span>,
        },
        enabled: <span class="hljs-keyword">import</span>.meta.env.NUXT_HOOKDECK_ENABLED === <span class="hljs-string">'1'</span>,
      }),
    ],
</code></pre>
<p>When we start the dev server, we will see the following screen:</p>
<pre><code class="lang-bash">Connections
xxx -&gt; xxx forwarding to /api/
                                                                                                                                                                                            4:07:08 PM
HOOKDECK URL MAPPING:                                                                                                                                                                       4:07:08 PM
┌─────────┬──────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────┐                                                                 4:07:08 PM
│ (index) │ Name     │ Hook                                             │ Dest                                            │
├─────────┼──────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────┤
│ 0       │ <span class="hljs-string">'Clerk'</span>  │ <span class="hljs-string">'https://hkdk.events/xxxxxxxxxxxx/hook/clerk'</span>  │ <span class="hljs-string">'http://localhost:3000/api/hook/clerk'</span>  │
│ 1       │ <span class="hljs-string">'Paddle'</span> │ <span class="hljs-string">'https://hkdk.events/xxxxxxxxxxxx/hook/paddle'</span> │ <span class="hljs-string">'http://localhost:3000/api/hook/paddle'</span> │
└─────────┴──────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────┘
✔ Nuxt Nitro server built <span class="hljs-keyword">in</span> 614ms                                                                                                                                                   nitro 4:07:08 PM
ℹ Vite server warmed up <span class="hljs-keyword">in</span> 1ms                                                                                                                                                             4:07:08 PM
ℹ Vite client warmed up <span class="hljs-keyword">in</span> 1ms
</code></pre>
<p>Since Nuxt restarts the Vite server repeatedly, this hookdeck will not be created multiple times; it will only be created once during runtime. Additionally, when you terminate the process using Ctrl + C, it will also be closed, preventing any memory leaks.</p>
<p>I put the plugin to Gist: <a target="_blank" href="https://gist.github.com/asika32764/1dc7fa9acb4dac6e31770b3e393ca972">https://gist.github.com/asika32764/1dc7fa9acb4dac6e31770b3e393ca972</a></p>
<p>Maybe I’ll convert it to NPM package after more projects tests.</p>
]]></content:encoded></item><item><title><![CDATA[Connect  Windows (XAMPP) MySQL from WSL2 Ubuntu]]></title><description><![CDATA[Install MySQL client on WSL Ubuntu.
sudo apt install mysql-client-core-8.0

Back yo Windows, run:
mysql -u root -p

Enter this command to enable root account for any host:
> CREATE USER 'root'@'%' IDENTIFIED BY 'root'; GRANT ALL PRIVILEGES ON *.* TO ...]]></description><link>https://lab.simular.co/connect-windows-mysql-from-wsl2</link><guid isPermaLink="true">https://lab.simular.co/connect-windows-mysql-from-wsl2</guid><category><![CDATA[MySQL]]></category><category><![CDATA[WSL]]></category><category><![CDATA[wsl2]]></category><category><![CDATA[Ubuntu]]></category><dc:creator><![CDATA[Simon Asika]]></dc:creator><pubDate>Thu, 19 Sep 2024 16:45:07 GMT</pubDate><content:encoded><![CDATA[<p>Install MySQL client on WSL Ubuntu.</p>
<pre><code class="lang-bash">sudo apt install mysql-client-core-8.0
</code></pre>
<p>Back yo Windows, run:</p>
<pre><code class="lang-bash">mysql -u root -p
</code></pre>
<p>Enter this command to enable root account for any host:</p>
<pre><code class="lang-bash">&gt; CREATE USER <span class="hljs-string">'root'</span>@<span class="hljs-string">'%'</span> IDENTIFIED BY <span class="hljs-string">'root'</span>; GRANT ALL PRIVILEGES ON *.* TO <span class="hljs-string">'root'</span>@<span class="hljs-string">'%'</span> WITH GRANT OPTION;
&gt; FLUSH PRIVILEGES;
&gt; <span class="hljs-built_in">exit</span>;
</code></pre>
<p>Next, on Wsl2, run:</p>
<pre><code class="lang-bash">mysql -u root -p -h <span class="hljs-string">"<span class="hljs-subst">$(hostname)</span>.local"</span>
</code></pre>
<p>This should works.</p>
<p>If still shows:</p>
<pre><code class="lang-bash">ERROR 2003 (HY000): Can<span class="hljs-string">'t connect to MySQL server on xxx</span>
</code></pre>
<p>Try install <code>libnss-mdns</code></p>
<pre><code class="lang-bash">sudo apt install libnss-mdns
</code></pre>
<p>Now you may want to enter the hostname to <code>.env</code> file, just type:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> $(hostname).<span class="hljs-built_in">local</span>
</code></pre>
<p>And copy the result to your <code>.env</code> file.</p>
]]></content:encoded></item><item><title><![CDATA[[PHP] DOMElement insert custom HTML using Symfony DomCrawler]]></title><description><![CDATA[In JavaScript, if we want to insert a custom HTML to an element, there is a convenience way that we can set it into the innerHTML.
const el = document.querySelector('.foo');

el.innerHTML = `<div>FOO</div>`;

But in PHP, although there has a DOM Docu...]]></description><link>https://lab.simular.co/php-domelement-insert-custom-html</link><guid isPermaLink="true">https://lab.simular.co/php-domelement-insert-custom-html</guid><category><![CDATA[PHP]]></category><category><![CDATA[DOM]]></category><category><![CDATA[Crawler]]></category><category><![CDATA[Symfony]]></category><category><![CDATA[HTML5]]></category><dc:creator><![CDATA[Simon Asika]]></dc:creator><pubDate>Fri, 19 Jul 2024 18:35:28 GMT</pubDate><content:encoded><![CDATA[<p>In JavaScript, if we want to insert a custom HTML to an element, there is a convenience way that we can set it into the <code>innerHTML</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'.foo'</span>);

el.innerHTML = <span class="hljs-string">`&lt;div&gt;FOO&lt;/div&gt;`</span>;
</code></pre>
<p>But in PHP, although there has a DOM Document implementation, it is only supports HTML4 and not up-to-date with W3C standards for a long time, the <code>innerHTML</code> still not work on PHP DOMDocument extension, so we must use another way to insert custom HTML.</p>
<h2 id="heading-using-symfony-domcrawler-for-html5">Using Symfony DomCrawler for HTML5</h2>
<p>The Symfony <a target="_blank" href="https://symfony.com/doc/current/components/dom_crawler.html">DomCrawler Component</a> is a useful package to help us parsing and filtering DOM, and there is a <a target="_blank" href="https://packagist.org/packages/masterminds/html5">masterminds/html5</a> package can work with DomCrawler to allow it correctly handle HTML5.</p>
<p>We can install both of them:</p>
<pre><code class="lang-bash">composer require symfony/dom-crawler masterminds/html5 symfony/css-selector
</code></pre>
<h2 id="heading-parse-html">Parse HTML</h2>
<p>Now we can parse a HTML string to DOMDocument:</p>
<pre><code class="lang-php"><span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">DomCrawler</span>\<span class="hljs-title">Crawler</span>;

<span class="hljs-comment">// Some HTML5 string, maybe is an article content, or get by HTTPClient</span>
$html = <span class="hljs-string">'...'</span>;

<span class="hljs-comment">// Parse it, you must add `useHtml5Parser: true` </span>
<span class="hljs-comment">// to use masterminds/html5 as the parser</span>
$crawler = <span class="hljs-keyword">new</span> Crawler($html, useHtml5Parser: <span class="hljs-literal">true</span>);

<span class="hljs-comment">// Filter what you want by CSS selector</span>
$cards = $crawler-&gt;filter(<span class="hljs-string">'.card'</span>);

<span class="hljs-keyword">foreach</span> ($cards <span class="hljs-keyword">as</span> $card) {
    <span class="hljs-comment">// @var $card DOMElement</span>
}
</code></pre>
<h2 id="heading-insert-or-replace-html">Insert or Replace HTML</h2>
<p>Now if you want to insert a simple HTML, you can just use <code>createElement()</code></p>
<pre><code class="lang-php">$body = $crawler-&gt;ownerDocument-&gt;createElement(<span class="hljs-string">'div'</span>);
$body-&gt;setAttribute(<span class="hljs-string">'class'</span>, <span class="hljs-string">'card-body'</span>);
$body-&gt;setAttribute(<span class="hljs-string">'style'</span>, <span class="hljs-string">'...'</span>);

<span class="hljs-comment">// Append child</span>
$card-&gt;appendChild($body);

<span class="hljs-comment">// Or insertBefore</span>
$card-&gt;firstChild-&gt;insertBefore($body);

<span class="hljs-comment">// Or replace</span>
$card-&gt;firstChild-&gt;replaceWith($body);
</code></pre>
<h2 id="heading-insert-from-another-dom">Insert from Another DOM</h2>
<p>And if you want to insert a HTML string, you must parse it into DOMElement first, here we use <code>Masterminds\HTML5</code> to parse it:</p>
<pre><code class="lang-php"><span class="hljs-keyword">use</span> <span class="hljs-title">Masterminds</span>\<span class="hljs-title">HTML5</span>;

$childHTML = <span class="hljs-string">'&lt;div class="card-body"&gt;...&lt;/div&gt;'</span>;

$html5 = <span class="hljs-keyword">new</span> HTML5();
$dom = $html5-&gt;loadHTML($childHTML);

<span class="hljs-comment">// Get the node</span>
$node = $dom-&gt;documentElement-&gt;firstElementChild;
</code></pre>
<p>If you directly insert this node:</p>
<pre><code class="lang-php">$card-&gt;appendChild($node);
</code></pre>
<p>you will receive an error message:</p>
<blockquote>
<p>Fatal error: Uncaught exception 'DOMException' with message 'Wrong Document Error'</p>
</blockquote>
<p>That because in the DOM standards, every DOM nodes and elements are belongs to only 1 DOMDocument, if you want to insert a DOM node into another Document, you must import them first.</p>
<pre><code class="lang-php"><span class="hljs-comment">// Now we get the ownerDocument of $card and import nodes</span>
<span class="hljs-comment">// Remember add the second argument TRUE to deeply import.</span>
$root = $card-&gt;ownerDocument-&gt;importNode($dom-&gt;documentElement, <span class="hljs-literal">true</span>);
</code></pre>
<p>The returned <code>$root</code> is a clone of original DOM node, we can insert it to new DOM:</p>
<pre><code class="lang-php">$card-&gt;appendChild($root-&gt;firstElementChild);
</code></pre>
<h2 id="heading-save-back-to-html">Save back to HTML</h2>
<p>In the current Symfony Crawler version, save as HTML can be very convenience, just use:</p>
<pre><code class="lang-php">$crawler-&gt;html();
</code></pre>
<p>Note, if you get the child crawler instance, it may print partial of the HTML:</p>
<pre><code class="lang-php">$cards-&gt;html();
</code></pre>
]]></content:encoded></item><item><title><![CDATA[How to resolve Imagick error when handling large image]]></title><description><![CDATA[When I'm trying to resolve a large image in PHP intervention/image package, sometimes I will receive a "Unable to decode input" error message, this message is too simple and not helpful.
To finding the cause, I found the matched Decoder class, and pr...]]></description><link>https://lab.simular.co/imagick-error-on-large-images</link><guid isPermaLink="true">https://lab.simular.co/imagick-error-on-large-images</guid><category><![CDATA[imagick]]></category><category><![CDATA[PHP]]></category><dc:creator><![CDATA[Simon Asika]]></dc:creator><pubDate>Fri, 19 Jul 2024 17:54:31 GMT</pubDate><content:encoded><![CDATA[<p>When I'm trying to resolve a large image in PHP <a target="_blank" href="https://image.intervention.io/">intervention/image</a> package, sometimes I will receive a <strong>"Unable to decode input"</strong> error message, this message is too simple and not helpful.</p>
<p>To finding the cause, I found the matched Decoder class, and print the Imagick real error message:</p>
<pre><code class="lang-php"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FilePathImageDecoder</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">NativeObjectDecoder</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">decode</span>(<span class="hljs-params">mixed $input</span>): <span class="hljs-title">ImageInterface</span>|<span class="hljs-title">ColorInterface</span>
    </span>{
        <span class="hljs-comment">// ...</span>
        <span class="hljs-keyword">try</span> {
            $imagick = <span class="hljs-keyword">new</span> Imagick();
            $imagick-&gt;readImage($input);
        } <span class="hljs-keyword">catch</span> (ImagickException $e) {
            var_dump($e-&gt;getMessage()); <span class="hljs-comment">// &lt;-- print here</span>
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> DecoderException(<span class="hljs-string">'Unable to decode input'</span>);
        }

        <span class="hljs-comment">// ...</span>
</code></pre>
<p>And the message shows: <code>width or height exceeds limit</code></p>
<p>Now I noticed that this is a limit from Imagick config, there is the solution that we can change this limit.</p>
<p>I'm using Ubuntu server, the Imagik config file is located at <code>/etc/ImageMagick-6/</code>, if you are using other platform, you must find the Imagick config position. Now, open <code>/etc/ImageMagick-6/policy.xml</code> and look at about line 62 and 63, this is the origin settings, the max width and height is <code>16000px</code>.</p>
<pre><code class="lang-xml">...
  <span class="hljs-tag">&lt;<span class="hljs-name">policy</span> <span class="hljs-attr">domain</span>=<span class="hljs-string">"resource"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"width"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"16KP"</span>/&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">policy</span> <span class="hljs-attr">domain</span>=<span class="hljs-string">"resource"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"height"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"16KP"</span>/&gt;</span>
...
</code></pre>
<p>In my case, the image I upload is <code>25000px</code> height, I change the value to:</p>
<pre><code class="lang-xml">...
  <span class="hljs-tag">&lt;<span class="hljs-name">policy</span> <span class="hljs-attr">domain</span>=<span class="hljs-string">"resource"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"width"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"32KP"</span>/&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">policy</span> <span class="hljs-attr">domain</span>=<span class="hljs-string">"resource"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"height"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"32KP"</span>/&gt;</span>
...
</code></pre>
<p>Save and leave this file, then remember to restart Apache:</p>
<pre><code class="lang-xml"># Ubuntu
service apahe2 restart
</code></pre>
<p>Now handling of the large image file is works.</p>
]]></content:encoded></item><item><title><![CDATA[How to Notarize Electron Mac App by CLI]]></title><description><![CDATA[Currently, if you create a Mac App, but not distributed it by Mac AppStore, Apple ask developers to notarize this app, otherwise the user who download your app and click to open it, will receive an alert: "mac cannot be opened because the developer c...]]></description><link>https://lab.simular.co/notarize-electron-mac-app</link><guid isPermaLink="true">https://lab.simular.co/notarize-electron-mac-app</guid><category><![CDATA[Electron]]></category><category><![CDATA[macOS]]></category><category><![CDATA[notarizing]]></category><dc:creator><![CDATA[Simon Asika]]></dc:creator><pubDate>Sat, 29 Jun 2024 13:44:48 GMT</pubDate><content:encoded><![CDATA[<p>Currently, if you create a Mac App, but not distributed it by Mac AppStore, Apple ask developers to notarize this app, otherwise the user who download your app and click to open it, will receive an alert: <strong>"mac cannot be opened because the developer cannot be verified"</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719668090270/fed42e0e-c60d-4877-b557-2ba8e31d9eaa.png" alt class="image--center mx-auto" /></p>
<p>This post described how to notarize a macOS app by CLI, you must build and sign you app first.</p>
<p>About how to sign Mac App, please see:</p>
<ul>
<li><p><a target="_blank" href="https://www.electronjs.org/docs/latest/tutorial/code-signing">https://www.electronjs.org/docs/latest/tutorial/code-signing</a></p>
</li>
<li><p><a target="_blank" href="https://nocommandline.com/blog/how-to-sign-an-electron-app-on-mac-with-electron-builder/">https://nocommandline.com/blog/how-to-sign-an-electron-app-on-mac-with-electron-builder/</a></p>
</li>
</ul>
<h2 id="heading-generate-app-specific-password">Generate App Specific Password</h2>
<p>Then, you must register an <a target="_blank" href="https://support.apple.com/102654">app specific password</a>, in this case, we store it into KeyChain, run:</p>
<pre><code class="lang-bash">xcrun altool --store-password-in-keychain-item <span class="hljs-string">"AC_PASSWORD"</span> \
  -u <span class="hljs-string">"<span class="hljs-variable">$your_applie_id</span>"</span> \
  -p <span class="hljs-string">"<span class="hljs-variable">$your_app_password</span>"</span>
</code></pre>
<p>Note, the <code>AC_PASSWORD</code> is the title of record in KeyChain, the <code>$your_applie_id</code> is your Apple ID Email, and <code>$your_app_password</code> should be the app specific password you have generated.</p>
<p>If you don't want to store it to KeyChain, you may replace all <code>AC_PASSWORD</code> as your password plain text when you run commands.</p>
<h2 id="heading-get-team-id">Get Team ID</h2>
<p>Run this command:</p>
<pre><code class="lang-bash">xcrun altool --list-providers -u <span class="hljs-string">"<span class="hljs-variable">$your_apple_id</span>"</span> -p <span class="hljs-string">"@keychain:AC_PASSWORD"</span>
</code></pre>
<p>You will see a team list, the column: <code>ProviderShortName</code> is your Team ID.</p>
<h2 id="heading-notarized-app">Notarized App</h2>
<p>After you build your Mac App (also, you must sign it with Developer ID Application), let's notarize it by CLI.</p>
<p>Run this command:</p>
<pre><code class="lang-bash">xcrun notarytool submit your-app.dmg --<span class="hljs-built_in">wait</span> --apple-id <span class="hljs-string">"<span class="hljs-variable">$your_apple_id</span>"</span> \
  --password <span class="hljs-string">"@keychain:AC_PASSWORD"</span> \
  --team-id <span class="hljs-string">"<span class="hljs-variable">$your_apple_team_id</span>"</span>;
</code></pre>
<p>This will take a while, and shows a process, please wait for it. After process finished, run the following command:</p>
<pre><code class="lang-bash">xcrun stapler staple your-app.dmg
</code></pre>
<p>Now, you app has been notarized, you can distribute your app to the web and let users download it.</p>
<h2 id="heading-for-electron-builder">For Electron Builder</h2>
<p>If you are using <code>electron-builder</code> , you can make this action automatic. Just install the package: <a target="_blank" href="https://www.npmjs.com/package/electron-builder-notarize">electron-builder-notarize</a></p>
<pre><code class="lang-bash">npm i electron-builder-notarize --save-dev
<span class="hljs-comment"># OR</span>
yarn add electron-builder-notarize --dev
</code></pre>
<p>And add this config to <code>electron-builder.config.json</code></p>
<pre><code class="lang-bash">{
  ...
  <span class="hljs-string">"mac"</span>: {
    ...
    // Add below 2 lines
    <span class="hljs-string">"hardenedRuntime"</span>: <span class="hljs-literal">true</span> ,
    <span class="hljs-string">"entitlements"</span> : <span class="hljs-string">"./node_modules/electron-builder-notarize/entitlements.mac.inherit.plist"</span>
  },
  <span class="hljs-string">"afterSign"</span> : <span class="hljs-string">"electron-builder-notarize"</span> // &lt;-- Add this line
  ...
}
</code></pre>
<p>And you must add an <code>.env</code> file (remember to ignore from git):</p>
<pre><code class="lang-bash">APPLE_ID=...
APPLE_ID_PASSWORD=...
APPLE_TEAM_ID=...
</code></pre>
<p>Now, when you pack electron app by electron-builder, it will auto notarize your app everytime.</p>
]]></content:encoded></item></channel></rss>