<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Hamish Milne</title>
    <description>The latest articles on Forem by Hamish Milne (@hamishmilne).</description>
    <link>https://forem.com/hamishmilne</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F773990%2Feeeedbe8-5926-4517-a567-367f9b50c067.png</url>
      <title>Forem: Hamish Milne</title>
      <link>https://forem.com/hamishmilne</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/hamishmilne"/>
    <language>en</language>
    <item>
      <title>Zig + CMake</title>
      <dc:creator>Hamish Milne</dc:creator>
      <pubDate>Mon, 16 Sep 2024 16:29:33 +0000</pubDate>
      <link>https://forem.com/hamishmilne/zig-cmake-4pmc</link>
      <guid>https://forem.com/hamishmilne/zig-cmake-4pmc</guid>
      <description>&lt;p&gt;I love &lt;a href="https://ziglang.org/" rel="noopener noreferrer"&gt;Zig&lt;/a&gt;. I'm a big fan of many (though not quite all!) of the design choices and philosophies behind the language.&lt;/p&gt;

&lt;p&gt;One particular capability I love is &lt;a href="https://ziglang.org/learn/overview/#cross-compiling-is-a-first-class-use-case" rel="noopener noreferrer"&gt;how easy it makes cross-compiling&lt;/a&gt; - both for Zig and C/C++ code. I have vivid memories of the pain involved in setting up cross-compilation toolchains, or just giving up and directly compiling directly on whatever Raspberry Pi I had to hand.&lt;/p&gt;

&lt;p&gt;Zig makes this as easy as &lt;code&gt;-DTarget=arm-linux-musleabihf&lt;/code&gt;, even from the locked-down Windows laptop I'm forced to use in my day job.&lt;/p&gt;

&lt;p&gt;Clang, which &lt;code&gt;zig cc&lt;/code&gt; wraps, has the same &lt;code&gt;-target&lt;/code&gt; option - but Zig goes a step further and includes dependencies for all the LibC variants it supports, removing the need for &lt;em&gt;any&lt;/em&gt; kind of cross-toolchain, headers, or system configuration.&lt;/p&gt;

&lt;p&gt;It's &lt;em&gt;magical&lt;/em&gt;. I genuinely believe that this will completely change developers' perspectives on cross-compilation. The "Cross compiling" section in every project's documentation will go from a multi-page guide to "here's a list of targets we've tested - go nuts".&lt;/p&gt;

&lt;p&gt;To illustrate this, let's take a use case that doesn't involve Zig-the-language at all. Say we're building a pre-existing app for some non-native platform - maybe changing some configuration, adding a patch or two, you know the drill.&lt;/p&gt;

&lt;p&gt;In many - dare I say most - cases, projects that proport to support multiple architectures, operating systems, and compiler toolchains will use CMake as a build system.&lt;/p&gt;

&lt;p&gt;(CMake has many, many faults. It is not 'good'. But it is currently the best 'mature', 'general-purpose' C/C++ build system by a long, long way.)&lt;/p&gt;

&lt;p&gt;In this case, you might define some top-level &lt;code&gt;CMakeLists.txt&lt;/code&gt; and add your dependencies as Git submodules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;cmake_minimum_required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;VERSION 3.22&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;my-project&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Adjust any configuration...&lt;/span&gt;
&lt;span class="nb"&gt;add_subdirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;my-dependency&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally, you'd now need to figure out how to get CMake to interact with your compiler toolchain of choice. This might involve running from a special command shell (i.e. Visual Studio), setting environment variables, relying on the built-in auto-detection, &lt;a href="https://vector-of-bool.github.io/docs/vscode-cmake-tools/kits.html" rel="noopener noreferrer"&gt;'kits'&lt;/a&gt;, et-cetera. It's a mess.&lt;/p&gt;

&lt;p&gt;However, with Zig available on the PATH, we can create a file we'll call &lt;code&gt;toolchain.cmake&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_C_COMPILER zig cc&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_CXX_COMPILER zig c++&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_C_COMPILER_TARGET &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_CXX_COMPILER_TARGET &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then add a single line to our main project file, just above the &lt;code&gt;project()&lt;/code&gt; statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_TOOLCHAIN_FILE &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CMAKE_CURRENT_SOURCE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/toolchain.cmake"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells CMake: don't rely on any system configuration. Use the compiler I've specified, and nothing else.&lt;/p&gt;

&lt;p&gt;We can then build the project in the usual way, but specifying whatever target we like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cmake &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-G&lt;/span&gt; Ninja &lt;span class="nt"&gt;-DTARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm-linux-musleabihf
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it. &lt;code&gt;zig cc&lt;/code&gt; ensures that LibC headers are present if needed, and since it's just Clang underneath, all the options that CMake passes in are well understood. We've just taken hours of awkward setup and debugging and replaced it with 5 lines of configuration.&lt;/p&gt;

&lt;p&gt;Just to make this really clear, this applies not just to cross-compiling, &lt;strong&gt;but native compiling as well&lt;/strong&gt;. As in, your development system setup for &lt;em&gt;any&lt;/em&gt; and &lt;em&gt;all&lt;/em&gt; targets is now just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;cmake ninja-build
&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;zig &lt;span class="nt"&gt;--classic&lt;/span&gt; &lt;span class="nt"&gt;--beta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Likewise for Windows, you only need to download binaries of &lt;a href="https://ziglang.org/download/" rel="noopener noreferrer"&gt;Zig&lt;/a&gt;, &lt;a href="https://cmake.org/download/" rel="noopener noreferrer"&gt;CMake&lt;/a&gt;, and &lt;a href="https://github.com/ninja-build/ninja/releases" rel="noopener noreferrer"&gt;Ninja&lt;/a&gt;, and put them on the PATH. No installers, no Visual Studio, no eternally-cursed Developer Command Prompt.&lt;/p&gt;

&lt;p&gt;If that's not magical, I don't know what is.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A long-winded Primer to the JavaScript Packaging Situation</title>
      <dc:creator>Hamish Milne</dc:creator>
      <pubDate>Sat, 22 Jun 2024 11:20:35 +0000</pubDate>
      <link>https://forem.com/hamishmilne/a-long-winded-primer-to-the-javascript-packaging-situation-32ho</link>
      <guid>https://forem.com/hamishmilne/a-long-winded-primer-to-the-javascript-packaging-situation-32ho</guid>
      <description>&lt;p&gt;If you've been dealing with the JavaScript ecosystem for more than 5 minutes you're probably aware that the situation with modules, build systems, runtimes, bundlers, and packaging code for distribution is kind of a mess right now. The aim of this guide is to cut through some of the bullshit, give a bunch of somewhat opinionated recommendations and end with a simple decision tree to let anyone determine what to write, what to distribute, and how to do it.&lt;/p&gt;

&lt;p&gt;First off, some key assumptions (aka, my opinions):&lt;/p&gt;

&lt;h2&gt;
  
  
  0. These are (just) my opinions
&lt;/h2&gt;

&lt;p&gt;I believe, quite strongly, that the approach outlined here should be used for all new JS projects. There will, &lt;em&gt;inevitably&lt;/em&gt;, be certain niche or legacy situations where you will &lt;em&gt;have&lt;/em&gt; to, for example, use UMD modules or TypeScript's 'import require' syntax. I'll try to note any examples where I've personally come across them, and of course feel free to comment your own if you have any.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. We are using TypeScript
&lt;/h2&gt;

&lt;p&gt;Seriously, at this point there is &lt;em&gt;precious&lt;/em&gt; little reason to not use TypeScript as your source language. The tooling is robust, the language itself is excellent, there's wide support in the ecosystem, and it's got first-class support in Bun (i.e. the probable NodeJS replacement).&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;really&lt;/em&gt; love runtime errors and want to see more of them in your code, you can set &lt;a href="https://www.typescriptlang.org/tsconfig#strict"&gt;&lt;code&gt;"strict": false&lt;/code&gt;&lt;/a&gt; in tsconfig and just... write JavaScript. Then, to be serious for a moment, you can gradually adopt type annotations as your project grows - and you'll have the systems in place to support it.&lt;/p&gt;

&lt;p&gt;In a few places, I'll be mentioning &lt;a href="https://typestrong.org/ts-node/"&gt;&lt;code&gt;ts-node&lt;/code&gt;&lt;/a&gt;, which is a module for NodeJS that adds a special 'loader' for TypeScript files. In essence, if you run Node with the right options (&lt;code&gt;node --loader ts-node/esm&lt;/code&gt; typically) you'll get pretty robust, seamless TypeScript support - no need for separate build steps. If it's at all applicable, you should use it.&lt;/p&gt;

&lt;p&gt;With all that said, I highly recommend &lt;em&gt;against&lt;/em&gt; &lt;em&gt;distributing&lt;/em&gt; raw TypeScript. At the end of the day, TypeScript is still an additional dependency that we don't want to impose on library consumers. There's also a plethora of configuration options that may cause distributed TypeScript to simply not work in a given downstream project.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. We don't care about UMD, AMD, SystemJS etc.
&lt;/h2&gt;

&lt;p&gt;For those not in the know, there are two 'module systems' (i.e. ways of telling the runtime what values to import and export from a given file) in common use right now. The normal, standard-since-2015, fully-featured, widely-adopted 'ECMAScript Module' syntax, or ESM for short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baz&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the old, outdated, legacy system used by NodeJS, called CommonJS (or CJS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baz&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There were once a whole plethora of alternative module systems: UMD, AMD, and SystemJS, but these are now historical footnotes. The only time you will encounter them is if someone has packaged a library using them (God help you!). You should never, in the current year, distribute code that uses them.&lt;/p&gt;

&lt;p&gt;And to be absolutely clear: the only reason we are even mentioning CommonJS is because the NodeJS implementation of ESM has, at the time of writing, several critical compatibility issues that are unlikely to be resolved in the near future, which will be explained in the relevant sections later on.&lt;/p&gt;

&lt;p&gt;And if you're using &lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt;, rejoice - you can mix and match both systems in the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not that you would want to, of course, but it's probably the best solution to the problems we'll be discussing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. We are aiming for simplicity, compatibility, and robustness
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;We want our emitted JS code to be as close to our TypeScript as possible.&lt;/li&gt;
&lt;li&gt;We don't want superfluous distribution files, or long build processes, or a load of extra dependencies.&lt;/li&gt;
&lt;li&gt;We want the code to work in every situation - as long as it doesn't conflict with the first two goals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The problem with ESM in NodeJS
&lt;/h1&gt;

&lt;p&gt;NodeJS uses CommonJS internally, and probably always will. Node's ESM support uses a special API hook called a 'loader', which is responsible for essentially translating your ESM files into CJS under the hood.&lt;/p&gt;

&lt;p&gt;There are, of course, several problems with this approach. Notably, you can't really configure it so that multiple loaders act on the same file, as a user. &lt;code&gt;ts-node&lt;/code&gt; has to use a special implementation to get ESM compatibility on Node, which is why you need &lt;code&gt;ts-node/esm&lt;/code&gt;. Likewise for Yarn's PnP system (though in that case it's the default). If you want to use both together, you're SOL.&lt;/p&gt;

&lt;p&gt;The second thing to remember is that ESM imports are inherently asynchronous, with top-level &lt;code&gt;await&lt;/code&gt; and so on, whereas Node's &lt;code&gt;require&lt;/code&gt; is a synchronous call. Evidently, Node's execution pipeline is inflexible enough that this presented an enormous problem for the devs, and the result: you can't &lt;code&gt;require&lt;/code&gt; ESM files in Node. The reverse isn't true, fortunately - you can &lt;code&gt;import&lt;/code&gt; CJS files no problem (well, some problems, but that's for later).&lt;/p&gt;

&lt;p&gt;In short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./foo.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Works!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./bar.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Error!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upshot is if you're writing apps and tools, you can, and &lt;em&gt;should&lt;/em&gt; run ESM natively. If you transpile to CJS, you won't be able to use ESM-only dependencies (without using &lt;code&gt;await import&lt;/code&gt;, which is a &lt;em&gt;whole&lt;/em&gt; other can of worms and will probably make your APIs unusable).&lt;/p&gt;

&lt;p&gt;But for libraries, where you have no idea if they'll be imported from an ESM or CJS context, you'll need to distribute your package as CJS by transpiling your ESM code (with &lt;code&gt;tsc&lt;/code&gt;, ideally). Note that in this case, &lt;strong&gt;ESM-only dependencies will not work&lt;/strong&gt; but they are, fortunately, quite rare.&lt;/p&gt;

&lt;p&gt;In all cases I've found, this transpiled CJS code will work just fine in bundlers like Webpack and Vite - in the latter case it'll get converted &lt;em&gt;back&lt;/em&gt; to ESM, funnily enough. The sole exception is Deno, which &lt;em&gt;does&lt;/em&gt; use ESM internally and won't like your CJS code. If you want to support Deno, bundlers, &lt;em&gt;and&lt;/em&gt; Node at runtime, there's no way around it: you'll have to multi-target CJS and ESM. But we'll get to that later.&lt;/p&gt;

&lt;p&gt;In conclusion: Node's ESM support sucks. At the very, very least, it's now officially 'stable'; the 'experimental' warnings you might remember in the Node CLI have gone away as of v20.&lt;/p&gt;

&lt;h1&gt;
  
  
  Configuration recommendations
&lt;/h1&gt;

&lt;p&gt;I'm going to be covering a few different types of JS project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend/web apps, &lt;a href="https://en.wikipedia.org/wiki/Immediately_invoked_function_expression"&gt;IIFE&lt;/a&gt; bundles&lt;/li&gt;
&lt;li&gt;Backend/server-side apps, tools, and local scripts&lt;/li&gt;
&lt;li&gt;Libraries

&lt;ul&gt;
&lt;li&gt;Some of which may need to run on NodeJS&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these has different requirements, so keep them in mind as you follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on import statements and file extensions
&lt;/h2&gt;

&lt;p&gt;What's the correct way to write imports of local files? It's easy, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Works... sometimes!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing to remember is that &lt;strong&gt;module resolution is not standardised across consumers&lt;/strong&gt;. All you're doing is telling whatever program is consuming your code - &lt;code&gt;tsc&lt;/code&gt;, Node, Webpack, etc. - to please give you the module object for &lt;code&gt;./foo&lt;/code&gt;, whatever that is. This is how when you import &lt;code&gt;react&lt;/code&gt;, Node will look inside &lt;code&gt;node_modules&lt;/code&gt; and load &lt;code&gt;node_modules/react/index.js&lt;/code&gt; or whatever.&lt;/p&gt;

&lt;p&gt;First off, if you're importing a local file, always begin the import specifier with &lt;code&gt;./&lt;/code&gt; or &lt;code&gt;../&lt;/code&gt;; this ensures there's no ambiguity with packages, and in my experience, makes tooling much more reliable. Don't try to mess with tsconfig to add multiple source roots, or do &lt;code&gt;src/foo&lt;/code&gt; or somesuch - you'll end up tying yourself in knots and run into all sorts of errors down the line. Stick to relative paths.&lt;/p&gt;

&lt;p&gt;Secondly, you need to be extremely cognizant of file extensions. This applies both to local files, and when deep-linking into a package to import a particular file (but not to directories or package roots). TypeScript, bundlers, &lt;a href="https://bun.sh/docs/runtime/modules#syntax"&gt;Bun&lt;/a&gt;, and &lt;a href="https://nodejs.org/api/modules.html#modules_file_modules"&gt;Node's &lt;code&gt;require&lt;/code&gt;&lt;/a&gt; statements will search for the file in question, trying different file extensions in a particular order until they get a match.&lt;/p&gt;

&lt;p&gt;Deno and Node's ESM implementation, however, do &lt;em&gt;not&lt;/em&gt; do this. They require the file to be specified exactly, including the specific file extension to use.&lt;/p&gt;

&lt;p&gt;Sounds reasonable - in the former cases, you aren't &lt;em&gt;forbidden&lt;/em&gt; from specifying the extension, it's just optional. So we just need to start using &lt;code&gt;import "./foo.js"&lt;/code&gt; and everything should still work... right?&lt;/p&gt;

&lt;p&gt;But we aren't writing JavaScript. We're writing TypeScript. So what should we use to import a local file - &lt;code&gt;.ts&lt;/code&gt;, or &lt;code&gt;.js&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The sad reality is that no option will work in all cases.&lt;/p&gt;

&lt;p&gt;In the IDE, you can use either: for &lt;code&gt;.js&lt;/code&gt;, TypeScript will 'magically know' to convert this to &lt;code&gt;.ts&lt;/code&gt; under the hood... ugh. You can also leave off the extension entirely, if TypeScript is configured to let you do this.&lt;/p&gt;

&lt;p&gt;If you're using &lt;code&gt;ts-node&lt;/code&gt; with &lt;code&gt;type: module&lt;/code&gt;, or Deno with TypeScript directly, you have to use &lt;code&gt;.ts&lt;/code&gt; because the &lt;code&gt;.js&lt;/code&gt; files don't exist on disk (the runtime will take care of the conversion).&lt;/p&gt;

&lt;p&gt;If you're manually transpiling to ESM JavaScript, then running on Node, you have to use &lt;code&gt;.js&lt;/code&gt;, because of course the &lt;code&gt;.ts&lt;/code&gt; files don't exist (and Node wouldn't understand them in any case).&lt;/p&gt;

&lt;p&gt;If you're manually transpiling to CJS, you can use &lt;code&gt;.js&lt;/code&gt;, or leave off the extension.&lt;/p&gt;

&lt;p&gt;If you're using a bundler that accepts TypeScript directly, like Webpack, you'll have to use &lt;code&gt;.ts&lt;/code&gt; because the &lt;code&gt;.js&lt;/code&gt; doesn't exist on disk. There are probably ways to rectify this with custom resolution rules etc. but that's out of scope for now.&lt;/p&gt;

&lt;p&gt;One 'simple' fix that comes to mind is simply rewriting import statements at build time from &lt;code&gt;*.ts&lt;/code&gt; (or blank) to &lt;code&gt;*.js&lt;/code&gt;. The TypeScript devs, however, &lt;a href="https://github.com/microsoft/TypeScript/issues/49083#issuecomment-1435399267"&gt;have explicitly rejected this&lt;/a&gt; as a solution - and their reasons do seem pretty solid.&lt;/p&gt;

&lt;p&gt;So, what's the TL;DR here?&lt;/p&gt;

&lt;p&gt;For libraries, &lt;strong&gt;you should specify &lt;code&gt;.js&lt;/code&gt;&lt;/strong&gt; because you'll be building JS files from your TS source anyway.&lt;/p&gt;

&lt;p&gt;For bundlers/front-end, &lt;strong&gt;you should leave off the extension, or use &lt;code&gt;.ts&lt;/code&gt;&lt;/strong&gt;. In this case you may need to set &lt;code&gt;"moduleResolution": "Bundler"&lt;/code&gt; as described below.&lt;/p&gt;

&lt;p&gt;For server-side that uses &lt;code&gt;ts-node&lt;/code&gt;, &lt;strong&gt;you should specify &lt;code&gt;.ts&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For server-side where you're pre-building your JS, &lt;strong&gt;you should specify &lt;code&gt;.js&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt;, and &lt;code&gt;moduleResolution&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These three fields, the first in package.json and the others in tsconfig.json, determine and are determined by the module system your compiled JavaScript will use. Despite them being three separate fields they are in fact very tightly linked, and only certain combinations will work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;type&lt;/code&gt; is used by both Node and TypeScript to decide what module system &lt;code&gt;.js&lt;/code&gt; and/or &lt;code&gt;.ts&lt;/code&gt; files use. If left unset, or set to &lt;code&gt;"commonjs"&lt;/code&gt; it will assume they're CJS. If set to &lt;code&gt;"module"&lt;/code&gt; it will assume they're ESM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For libraries targeting Node and therefore emitting CJS, you should set it to &lt;code&gt;commonjs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In all other cases, use &lt;code&gt;module&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setting can be overridden on a per-file basis by using &lt;code&gt;.cjs&lt;/code&gt; and &lt;code&gt;.cts&lt;/code&gt; to for CJS code, and &lt;code&gt;.mjs&lt;/code&gt; and &lt;code&gt;.mts&lt;/code&gt; for ESM code. This is useful, but these extensions are not as widely-supported and there are pitfalls associated with using them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.typescriptlang.org/tsconfig#moduleResolution"&gt;&lt;code&gt;moduleResolution&lt;/code&gt;&lt;/a&gt; determines how TypeScript resolves type information from an &lt;code&gt;import&lt;/code&gt; operand, and ensures that these operands are specified correctly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For server-side, we need to use &lt;code&gt;Node16&lt;/code&gt;, since this allows us to import both ESM and CJS and will give compile errors when we fail to specify a file extension.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For libraries building to CJS, we need to use &lt;code&gt;Node10&lt;/code&gt;, since that's the only option allowed when doing so.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For front-end, or libraries only targeting front-end, you should use &lt;code&gt;Bundler&lt;/code&gt;, which is similar to &lt;code&gt;Node16&lt;/code&gt; but more permissive.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.typescriptlang.org/tsconfig#module"&gt;&lt;code&gt;module&lt;/code&gt;&lt;/a&gt; determines which module system &lt;code&gt;tsc&lt;/code&gt; will use in the emitted JS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For server-side, we need to use &lt;code&gt;Node16&lt;/code&gt; because this is the only option when &lt;code&gt;moduleResolution&lt;/code&gt; is correctly set for this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For libraries building to CJS we should, of course, use &lt;code&gt;CommonJS&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For other libraries, you should use &lt;code&gt;ES2015&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And for front-end you can use &lt;code&gt;ES2022&lt;/code&gt;, or otherwise the latest version.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, what do I save my source files as?
&lt;/h2&gt;

&lt;p&gt;If you follow the steps above, you should save your files as &lt;code&gt;.ts&lt;/code&gt;, as normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;target&lt;/code&gt; and &lt;code&gt;lib&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;target&lt;/code&gt; tells &lt;code&gt;tsc&lt;/code&gt; whether it needs to down-level newer JavaScript features to support older runtimes.&lt;/p&gt;

&lt;p&gt;For any app or tool in which you control the runtime environment, you should set it to the latest: &lt;code&gt;ES2022&lt;/code&gt; at the time of writing.&lt;/p&gt;

&lt;p&gt;For libraries, or code that gets distributed like a dev tool, you should start with &lt;code&gt;ES2018&lt;/code&gt; (essentially ES2015 but with async functions and object-spread expressions, both supported since Node v10) and only increment it as needed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;lib&lt;/code&gt; option should be kept in sync with &lt;code&gt;target&lt;/code&gt; (aside from the addition of &lt;code&gt;DOM&lt;/code&gt;, &lt;code&gt;DOM.Iterable&lt;/code&gt; for front-end).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;isolatedModules&lt;/code&gt; and &lt;code&gt;verbatimModuleSyntax&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Turning TypeScript into JavaScript is, in most cases, quite straightforward: find any syntax that related to the type system, and delete it. This works because TypeScript was always designed to be a strict superset of JavaScript, and it's great because the JavaScript we emit is extremely close to the source code. So if sourcemaps aren't working you can still debug, and you don't need to worry about &lt;code&gt;tsc&lt;/code&gt; introducing bugs into your code.&lt;/p&gt;

&lt;p&gt;Of course if you're targeting CJS you need to turn all your &lt;code&gt;import&lt;/code&gt;s into &lt;code&gt;require&lt;/code&gt;s, but that's pretty straightforward to do, and in any case we'll deal with the pitfalls later on.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;tsc&lt;/code&gt; runs, it needs to load up every file in the project in order to type-check everything. If successful, it then emits the JS code for every file. Other tools, like swc, esbuild, and Vite, don't do this: they process each file individually and in parallel, without type-checking, often in response to those specific files being changed. As you can imagine this is enormously faster. You can then run &lt;code&gt;tsc&lt;/code&gt; on a slower cadence, without emitting JS. This is known as 'SFC' for Single-File Compilation.&lt;/p&gt;

&lt;p&gt;However, there are a couple of TypeScript language features that don't fit this pattern: &lt;code&gt;namespace&lt;/code&gt; and &lt;code&gt;const enum&lt;/code&gt;. I won't go into details here, but suffice it to say that these are now largely considered mistakes, and will often cause errors if your TypeScript is consumed by anything other than &lt;code&gt;tsc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To fix this, you should always enable &lt;code&gt;isolatedModules&lt;/code&gt; in tsconfig, which makes it an error to use any of these features.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second problem relates to importing types. &lt;a href="https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax"&gt;The documentation&lt;/a&gt; goes into detail, but the short version is that, when importing, you should be explicit about importing a type, compared to a runtime value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of this:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;some_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;some_value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;other_type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Use this:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;some_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;some_value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;other_type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's the difference between &lt;code&gt;import type { foo }&lt;/code&gt; and &lt;code&gt;import { type foo }&lt;/code&gt;? The first one will get removed entirely from the output, whereas the second one will become &lt;code&gt;import {}&lt;/code&gt;. This is crucial to remember if module initialization order matters - for example if a module has any side effects (it's extremely bad practice, but... it happens).&lt;/p&gt;

&lt;p&gt;My personal advice: if you're importing modules with side effects, use &lt;code&gt;import "foo";&lt;/code&gt; with a comment to explain; ideally at the very top of your program.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setting &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; will helpfully enforce the use of &lt;code&gt;import type&lt;/code&gt; for us, so you should absolutely enable it in tsconfig.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;BUT!&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; is enabled, &lt;code&gt;tsc&lt;/code&gt; will - &lt;em&gt;bafflingly&lt;/em&gt; - refuse to emit CJS code! The documentation implies that this is a deliberate design choice, which is very silly indeed.&lt;/p&gt;

&lt;p&gt;This means that, if we're targeting CJS, we need to do a little dance with the compiler by defining a secondary tsconfig that we'll call &lt;code&gt;tsconfig-cjs.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"verbatimModuleSyntax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CommonJS"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll then need to set &lt;code&gt;"module": "ES2018"&lt;/code&gt; in the main tsconfig, so that the IDE doesn't complain. The build command will be &lt;code&gt;npx tsc -p ./tsconfig-cjs.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  LFANs, &lt;code&gt;esModuleInterop&lt;/code&gt;, and &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Normally, when converting ESM to CJS the rules are pretty straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;baz&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bat2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// dist/index.cjs&lt;/span&gt;
&lt;span class="c1"&gt;// ... Okay, it's not *exactly* this, there's normally some extra cruft, but semantically this is what you get.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baz&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bat2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note a couple of things here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a 'Default Import' (&lt;code&gt;import baz&lt;/code&gt;) in ESM is the same as accessing the &lt;code&gt;default&lt;/code&gt; property of the module object in CJS&lt;/li&gt;
&lt;li&gt;a 'Namespace Import' (&lt;code&gt;* as bar&lt;/code&gt;) in ESM is the same as just assigning the whole module to a variable in CJS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are both true because the CJS export syntax is simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all fine, but CJS doesn't restrict &lt;code&gt;module.exports&lt;/code&gt; to only objects - it can be anything you like, a string, a number, a function. Indeed, a common pattern historically was to define modules like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PackageFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// index.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PackageFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;package&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myPackage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PackageFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Let's call this a... Legacy Function-As-Namespace Module. LFAN for short.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Of course, it's not possible to define LFANs in ESM, but we can at least consume them: simply use &lt;code&gt;import * as PackageFactory from "package"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However... this is &lt;em&gt;technically&lt;/em&gt; invalid according to the ESM spec! ESM namespaces are immutable object-like things and must &lt;em&gt;never&lt;/em&gt; be callable!!!1!&lt;/p&gt;

&lt;p&gt;Therefore, transpilers like &lt;code&gt;tsc&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;swc&lt;/code&gt;, and Babel, will usually &lt;a href="https://www.typescriptlang.org/tsconfig#esModuleInterop"&gt;generates a lot of cruft&lt;/a&gt; in CJS output in order to 'fix' this 'problem', which is controlled with the &lt;code&gt;esModuleInterop&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;There are, roughly speaking, two methods used to do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When importing a CJS module which has no &lt;code&gt;default&lt;/code&gt; field, set &lt;code&gt;module.exports.default = module.exports&lt;/code&gt;, OR&lt;/li&gt;
&lt;li&gt;Create a new, immutable object, copying over only the owned properties of &lt;code&gt;module.exports&lt;/code&gt;, if any, and setting the value of &lt;code&gt;default&lt;/code&gt; to &lt;code&gt;module.exports&lt;/code&gt; itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, and &lt;code&gt;swc&lt;/code&gt; use the first option, which is more efficient and compatible, since it ends up that a Default Import and a Namespace Import will provide the same value. Node's ESM runtime and Babel use the second, which is more spec-compliant, since &lt;em&gt;only&lt;/em&gt; a Default Import will work - a Namespace Import will return an object like &lt;code&gt;{ default: moduleFunction }&lt;/code&gt;, which is probably not what you expect.&lt;/p&gt;

&lt;p&gt;From what I can tell, only &lt;code&gt;tsc&lt;/code&gt; allows you to turn this 'feature' off entirely, by disabling &lt;code&gt;esModuleInterop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;TypeScript's &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; mirrors this runtime behaviour in the type system. When enabled, TypeScript relaxes its checking of import statements very slightly, such that for any module that lacks an explicit 'default' export, a 'synthetic' one is created that just aliases the whole module. Unfortunately there's no way to forbid Namespace Imports in this situation, so if you're targeting Node's ESM or Babel you just need to be careful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// allowed when allowSyntheticDefaultImports=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, in general:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you know that you have no LFAN dependencies, OR&lt;/li&gt;
&lt;li&gt;You are only targeting CJS, and don't intend to run your source code through &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;swc&lt;/code&gt;, or Babel,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then you can (and should) disable both &lt;code&gt;esModuleInterop&lt;/code&gt; and &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt;. &lt;strong&gt;Otherwise, you will have to enable them both to prevent compatibility issues down the line, and be sure to only use Default Imports for LFANs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Note that, for CJS emits, TypeScript will always output the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;__esModule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This just sets a flag that allows consumers that use &lt;code&gt;esModuleInterop&lt;/code&gt; (and similar) to be a little more efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other, miscellaneous options
&lt;/h2&gt;

&lt;p&gt;You should enable &lt;a href="https://www.typescriptlang.org/tsconfig#composite"&gt;&lt;code&gt;composite&lt;/code&gt;&lt;/a&gt;, and set your &lt;code&gt;include&lt;/code&gt; patterns appropriately. This will also enable &lt;a href="https://www.typescriptlang.org/tsconfig#incremental"&gt;&lt;code&gt;incremental&lt;/code&gt;&lt;/a&gt; (making your builds faster), so remember to add &lt;code&gt;.tsbuildinfo&lt;/code&gt; to gitignore. If all your code is in some directory, e.g. &lt;code&gt;src&lt;/code&gt;, make sure to set &lt;code&gt;rootDir&lt;/code&gt; to that aforementioned directory.&lt;/p&gt;

&lt;p&gt;You will probably want to enable &lt;a href="https://www.typescriptlang.org/tsconfig#skipLibCheck"&gt;&lt;code&gt;skipLibCheck&lt;/code&gt;&lt;/a&gt;. Packages can include all sorts of TypeScript code which may or may not be valid for your current TypeScript version and compilation options, so using this saves a lot of headaches. Note you'll still have type-checking on your &lt;em&gt;imports&lt;/em&gt; of library code - this just tells &lt;code&gt;tsc&lt;/code&gt; to not bother checking the library declaration files themselves for correctness.&lt;/p&gt;

&lt;p&gt;There are various other useful options like &lt;code&gt;forceConsistentCasingInFileNames&lt;/code&gt; but these all come with sensible defaults.&lt;/p&gt;

&lt;p&gt;And finally you should, of course, enable &lt;a href="https://www.typescriptlang.org/tsconfig#strict"&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/a&gt; in tsconfig.&lt;/p&gt;

&lt;h1&gt;
  
  
  Build, run, distribute
&lt;/h1&gt;

&lt;h2&gt;
  
  
  You should use &lt;code&gt;ts-node&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For server-side apps, or local scripts, this is without a doubt the best approach.&lt;/strong&gt; It's simple, robust, and removes whole classes of potential errors.&lt;/p&gt;

&lt;p&gt;With everything set up as described above, you'd just run &lt;code&gt;node --loader ts-node/esm ./my_file.ts&lt;/code&gt;. Debugging in VSCode 'just works'. I like to use the JavaScript Debug Terminal as it saves having to create &lt;code&gt;launch.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;ts-node&lt;/code&gt; is all you need, you should enable &lt;a href="https://www.typescriptlang.org/tsconfig#noEmit"&gt;&lt;code&gt;noEmit&lt;/code&gt;&lt;/a&gt; in tsconfig, and use &lt;code&gt;tsc&lt;/code&gt; as essentially a linter. This prevents JavaScript files from being created accidentally.&lt;/p&gt;

&lt;p&gt;If you're using Yarn, remember to set &lt;a href="https://yarnpkg.com/configuration/yarnrc#nodeLinker"&gt;&lt;code&gt;nodeLinker: node-modules&lt;/code&gt;&lt;/a&gt; in yarnrc, for the reasons mentioned above about Node's ESM support sucking.&lt;/p&gt;

&lt;p&gt;The only &lt;code&gt;ts-node&lt;/code&gt; specific option I'd recommend is &lt;a href="https://typestrong.org/ts-node/docs/options/#transpileonly"&gt;&lt;code&gt;transpileOnly&lt;/code&gt;&lt;/a&gt;. You probably don't need &lt;code&gt;swc&lt;/code&gt;, but if startup performance is really critical you might consider it.&lt;/p&gt;

&lt;p&gt;There are of course certain situations where &lt;code&gt;ts-node&lt;/code&gt; isn't an option. Maybe you &lt;em&gt;really&lt;/em&gt; want to use Yarn PnP, or you're distributing a dev tool where you want to minimise your runtime dependencies. In that case...&lt;/p&gt;

&lt;h2&gt;
  
  
  Emitting ESM
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You'll need to do this for dev tools, and for libraries which don't target Node as a runtime (or do, but have ESM-only dependencies).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With everything configured as above, just set &lt;a href="https://www.typescriptlang.org/tsconfig#outDir"&gt;&lt;code&gt;outDir&lt;/code&gt;&lt;/a&gt;, and run &lt;code&gt;tsc&lt;/code&gt; to build. If you take a look at the emitted JavaScript you'll see that it's extremely close to your source code.&lt;/p&gt;

&lt;p&gt;For an app, you can then run &lt;code&gt;node ./dist/main.js&lt;/code&gt; as normal. You should explicitly set &lt;a href="https://www.typescriptlang.org/tsconfig#declaration"&gt;&lt;code&gt;declaration&lt;/code&gt;&lt;/a&gt; to false, since you're not expecting anyone to consume your type information, and it'll speed up your build process.&lt;/p&gt;

&lt;p&gt;For a library, you'll want to set two fields in package.json, &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;types&lt;/code&gt;, which should point to &lt;code&gt;/dist/index.js&lt;/code&gt; and &lt;code&gt;/dist/index.d.ts&lt;/code&gt; respectively. This allows consumers to import your library and get the root module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emitting CJS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You'll need to do this for libraries that target Node, but not Deno, and don't have any ESM-only dependencies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After following the steps described in the &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; section above, you should already have a &lt;code&gt;tsconfig-cjs.json&lt;/code&gt; file. Simply run &lt;code&gt;npx tsc -p ./tsconfig-cjs.json&lt;/code&gt; and you'll have your output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-targeting CJS and ESM
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You'll need to do this if you want to target both Node and Deno.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since CJS is the 'lowest common denominator' it will be your primary target, with ESM being built specifically for Deno. Follow all the steps above assuming you're targeting CJS alone.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;tsconfig-cjs.json&lt;/code&gt;, set your &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist/cjs&lt;/code&gt; or similar. Set &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;types&lt;/code&gt; in package.json accordingly. Then in your main &lt;code&gt;tsconfig.json&lt;/code&gt;, set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist/esm&lt;/code&gt; or similar.&lt;/p&gt;

&lt;p&gt;We can then run &lt;code&gt;npx tsc&lt;/code&gt; for ESM, and &lt;code&gt;npx tsc -p ./tsconfig-cjs.json&lt;/code&gt; for CJS.&lt;/p&gt;

&lt;p&gt;Remember: since we've set &lt;code&gt;type: commonjs&lt;/code&gt;, if you try to import the contents of &lt;code&gt;dist/esm&lt;/code&gt; in Node you'll get an error. Of course there's no reason to do this when the CJS target works just fine and is more compatible in any case. The only consumers of this target will be those that outright do not support CJS (i.e. Deno).&lt;/p&gt;

&lt;p&gt;Newer versions of Node use a field called &lt;a href="https://nodejs.org/docs/latest-v18.x/api/packages.html#conditional-exports"&gt;&lt;code&gt;exports&lt;/code&gt;&lt;/a&gt; in package.json, which is supposed to allow CJS contexts to get a CJS module, and ESM contexts to get an ESM module. I don't see much value in this when we know CJS works all the time; it just seems like it'll make testing harder.&lt;/p&gt;

&lt;h1&gt;
  
  
  Things to avoid
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Don't use &lt;code&gt;export =&lt;/code&gt; or &lt;code&gt;import foo = require&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before TypeScript supported ESM, there was some special syntax to define a CJS module's imports and exports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// .js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... Yep, that's really all it does. Honestly I don't see the point.&lt;/p&gt;

&lt;p&gt;In any case, this syntax is still available today, and it's the only way to define a 'raw' CJS module in TypeScript, rather than an ESM module which is then converted to CJS. As you might imagine, &lt;strong&gt;you can only target CJS when using this syntax&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Furthermore, though the &lt;a href="https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require"&gt;documentation&lt;/a&gt; doesn't call this out, there's no way to import or export type-only declarations (&lt;code&gt;type&lt;/code&gt; and &lt;code&gt;interface&lt;/code&gt;) with this syntax.&lt;/p&gt;

&lt;p&gt;There are exactly two reasons why you'd want to use this rather than ESM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You really hate the presence of &lt;code&gt;Object.defineProperty(exports, "__esModule", { value: true });&lt;/code&gt; in the output (with this syntax it won't be emitted)&lt;/li&gt;
&lt;li&gt;You want to define an LFAN, so you need a way to directly control the value of &lt;code&gt;module.exports&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither of these are very good reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't bundle libraries for distribution
&lt;/h2&gt;

&lt;p&gt;Some library authors follow the practice of bundling some or all of a library's local files and/or package dependencies into the output. There is really only one valid purpose for doing this, and that's when you're creating a bundle for legacy web development - the sort where you write &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags to include your dependencies then manually bash out the JavaScript.&lt;/p&gt;

&lt;p&gt;This is, of course, a very outdated technique and should be discouraged at every opportunity in favour of just... using NPM. However, if it does become necessary, you need to create a global-setting IIFE bundle, as with Webpack's &lt;a href="https://webpack.js.org/configuration/output/#type-global"&gt;&lt;code&gt;type: global&lt;/code&gt;&lt;/a&gt; or &lt;code&gt;esbuild&lt;/code&gt;'s &lt;a href="https://esbuild.github.io/api/#global-name"&gt;global name&lt;/a&gt;. This bundle can be distributed in addition to a standard NPM package. You should under no circumstances use something like UMD; CommonJS consumers should always use packages.&lt;/p&gt;

&lt;p&gt;In all other cases, there is absolutely no reason to do this. It complicates dependency management, makes debugging harder, and requires additional tooling for no real benefit in kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be extremely careful with &lt;code&gt;await import&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;await import&lt;/code&gt; syntax is ESM's method of defining a 'dynamic import' - that is, an import that happens during the program flow, rather than immediately on startup.&lt;/p&gt;

&lt;p&gt;The most common use of this syntax is for 'code splitting' on the front-end: JS bundles tend to be rather large, so this allows you to lazy-load parts of your app to make your initial page load faster (the bundler is responsible for figuring out how this works underneath).&lt;/p&gt;

&lt;p&gt;The other use case is specific to NodeJS. The Node runtime provides &lt;code&gt;await import&lt;/code&gt; in ESM contexts... &lt;strong&gt;but also in CJS contexts&lt;/strong&gt;. And, unlike &lt;code&gt;require&lt;/code&gt;, &lt;strong&gt;you can import &lt;em&gt;any&lt;/em&gt; kind of module using it&lt;/strong&gt;, both ESM and CJS. So the upshot is that, under Node, this is the only way to import an ESM module into a CJS context.&lt;/p&gt;

&lt;p&gt;Let's consider an example dependency diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main.mjs
┣━ import 'A.mjs'
┃  ┗━ import 'B.cjs'
┃     ┗━ require('C.cjs')
┗━ import 'D.cjs'
   ┗━ await import('E.mjs')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to import module E in the context of module D, we have to use &lt;code&gt;await import&lt;/code&gt;. It's a useful escape hatch for the very few cases where you have no other options.&lt;/p&gt;

&lt;p&gt;However, you do have other options!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you're making a server-side app, just use ESM directly as recommended earlier.&lt;/strong&gt; There's no reason not do do this now that Node's ESM is considered stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you're making a library, and you &lt;em&gt;really&lt;/em&gt; can't get rid of this ESM-only dependency, just make your library ESM-only too.&lt;/strong&gt; While this does just kick the problem further up the chain, your consumers &lt;em&gt;can also&lt;/em&gt; just use ESM directly. In other words, if anyone complains, direct them to the point above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that &lt;code&gt;await import&lt;/code&gt; is really, really awful to use outside of code-splitting. You end up with code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// there are other ways, but this is probably the most elegant&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doImports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// note that all our exports need to be async functions now.&lt;/span&gt;
&lt;span class="c1"&gt;// want to export sync functions, or constants? You're SOL.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;myFunc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;doImports&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is, to put it lightly, horrible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't downlevel to ES5
&lt;/h2&gt;

&lt;p&gt;ES2015, aka ES6, has been standardised since (as the name implies) 2015. NodeJS has been &lt;a href="https://node.green/"&gt;99% compliant since version 8&lt;/a&gt;. Any browser version released after 2016, &lt;strong&gt;eight years ago&lt;/strong&gt;, &lt;a href="https://caniuse.com/?search=es6"&gt;fully supports it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;only&lt;/em&gt; reason to target ES5 is to support ancient, insecure, unsupported browsers like IE11. If this applies to you, I understand that it's unlikely to be by choice, so you have my sympathies :)&lt;/p&gt;

&lt;h1&gt;
  
  
  A note on unit testing
&lt;/h1&gt;

&lt;p&gt;Traditionally, the received wisdom for TypeScript unit testing was to use &lt;a href="https://kulshekhar.github.io/ts-jest/"&gt;&lt;code&gt;ts-jest&lt;/code&gt;&lt;/a&gt;. However, Jest is kind of enormous and brittle with a huge amount of configuration for some reason, and in my experience getting both &lt;a href="https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/"&gt;TypeScript&lt;/a&gt; and &lt;a href="https://jestjs.io/docs/ecmascript-modules"&gt;ESM&lt;/a&gt; to work with Jest really quite painful.&lt;/p&gt;

&lt;p&gt;For that reason, &lt;strong&gt;I highly recommend &lt;a href="https://vitest.dev/"&gt;Vitest&lt;/a&gt;&lt;/strong&gt;. It's compatible with Jest's API and has both TypeScript and ESM support out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you really have to use Jest for some reason, you should target CJS&lt;/strong&gt; and save yourself a lot of headaches.&lt;/p&gt;

&lt;h1&gt;
  
  
  In summary...
&lt;/h1&gt;

&lt;h2&gt;
  
  
  In all cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;typescript&lt;/code&gt; as a dependency&lt;/li&gt;
&lt;li&gt;Save your files as &lt;code&gt;.ts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write normal TypeScript using the ESM syntax&lt;/li&gt;
&lt;li&gt;When importing a local file, always use a relative path starting with &lt;code&gt;./&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;strict&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;isolatedModules&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;skipLibCheck&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;composite&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, making sure to set &lt;code&gt;include&lt;/code&gt; and &lt;code&gt;rootDir&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frontend/web apps (or anything using a bundler)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Refer to your specific bundler (e.g. Webpack, Vite, esbuild) for rules about file extensions in import statements&lt;/li&gt;
&lt;li&gt;In package.json, set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"module"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; and &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2022"&lt;/code&gt; (or newer)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Bundler"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;noEmit&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Let the bundler consume your TypeScript directly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Libraries that are only used by Web apps, Bun, Deno etc., or target Node but have ESM-only dependencies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;.js&lt;/code&gt; file extensions when importing local modules&lt;/li&gt;
&lt;li&gt;In package.json:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"module"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;"./dist/index.js"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;types&lt;/code&gt; to &lt;code&gt;"./dist/index.d.ts"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; and &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Node16"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2018"&lt;/code&gt; (or &lt;code&gt;ES2015&lt;/code&gt; if absolutely required)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;lib&lt;/code&gt; to the smallest required set of libraries&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;sourceMap&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npx tsc&lt;/code&gt; to build&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Libraries that may be used by the NodeJS runtime
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;.js&lt;/code&gt; file extensions when importing local modules&lt;/li&gt;
&lt;li&gt;In package.json:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"commonjs"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;"./dist/index.js"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;types&lt;/code&gt; to &lt;code&gt;"./dist/index.d.ts"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; to &lt;code&gt;"ES2015"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2018"&lt;/code&gt; (or &lt;code&gt;ES2015&lt;/code&gt; if absolutely required)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Node10"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;lib&lt;/code&gt; to the smallest required set of libraries&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;sourceMap&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consider setting &lt;code&gt;esModuleInterop&lt;/code&gt; and &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;, making sure to use Namespace Imports for LFANs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Create a file &lt;code&gt;tsconfig-cjs.json&lt;/code&gt;, extending the primary one, and in it:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; to &lt;code&gt;"CommonJS"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npx tsc -p ./tsconfig-cjs.json&lt;/code&gt; to build&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Libraries that may be used by the NodeJS runtime, but also want to target Deno
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;.js&lt;/code&gt; file extensions when importing local modules&lt;/li&gt;
&lt;li&gt;In package.json:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"commonjs"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;main&lt;/code&gt; to &lt;code&gt;"./dist/cjs/index.js"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;types&lt;/code&gt; to &lt;code&gt;"./dist/cjs/index.d.ts"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; to &lt;code&gt;"ES2015"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2018"&lt;/code&gt; (or &lt;code&gt;ES2015&lt;/code&gt; if absolutely required)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Node"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;lib&lt;/code&gt; to the smallest required set of libraries&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist/esm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;sourceMap&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If you have any LFANs as dependencies, or may do in future, set &lt;code&gt;esModuleInterop&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;; otherwise &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Create a file &lt;code&gt;tsconfig-cjs.json&lt;/code&gt;, extending the primary one, and in it:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; to &lt;code&gt;"CommonJS"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist/cjs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npx tsc -p ./tsconfig-cjs.json&lt;/code&gt; to build the CJS target, and &lt;code&gt;npx tsc&lt;/code&gt; to build the ESM&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Server-side apps and local scripts where you can use &lt;code&gt;ts-node&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;.ts&lt;/code&gt; file extensions when importing local modules&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;ts-node&lt;/code&gt; as a dependency&lt;/li&gt;
&lt;li&gt;If using Yarn, set &lt;code&gt;nodeLinker&lt;/code&gt; to &lt;code&gt;node-modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In package.json, set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"module"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; and &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Node16"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2022"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;noEmit&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Start the app with &lt;code&gt;node --loader ts-node/esm src/main.mts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dev tools, server-side apps that can't use &lt;code&gt;ts-node&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;.js&lt;/code&gt; file extensions when importing local modules&lt;/li&gt;
&lt;li&gt;In package.json, set &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;"module"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In tsconfig:

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;module&lt;/code&gt; and &lt;code&gt;moduleResolution&lt;/code&gt; to &lt;code&gt;"Node16"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;target&lt;/code&gt; to &lt;code&gt;"ES2022"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;allowSyntheticDefaultImports&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;outDir&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;sourceMap&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;npx tsc&lt;/code&gt; to build your JavaScript before running your app as normal&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>npm</category>
    </item>
    <item>
      <title>Making a better 2D asset pipeline</title>
      <dc:creator>Hamish Milne</dc:creator>
      <pubDate>Tue, 21 Dec 2021 20:54:10 +0000</pubDate>
      <link>https://forem.com/hamishmilne/making-a-better-2d-asset-pipeline-5d3e</link>
      <guid>https://forem.com/hamishmilne/making-a-better-2d-asset-pipeline-5d3e</guid>
      <description>&lt;h2&gt;
  
  
  I hate image formats.
&lt;/h2&gt;

&lt;p&gt;Maybe it's an emotional reaction to hours upon hours of searching for obscure specifications, finding bizarre proprietary blobs in supposedly standardised files, wondering if the tool I'm running is even doing anything, and having my (fairly high end) PC completely run out of memory several times - but it's an understandable one, I think.&lt;/p&gt;

&lt;p&gt;As a warning, this post is going to be pretty long, boring, and rant-y. If that's not for you, feel free to skip it and go look at some cool WebGL demo, but if you like this sort of thing then read on!&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of background.
&lt;/h2&gt;

&lt;p&gt;After a long day of coding, I like to sit down for a nice evening of coding - specifically, working on a to-be-announced game project along with a couple of long-time friends. Naturally, I've ended up in the architect's role, which means I'm responsible for our asset pipelines, and in the (primarily) 2D game we're making, that means a &lt;em&gt;lot&lt;/em&gt; of sprites.&lt;/p&gt;

&lt;p&gt;In our game, each character bust is built from a whole load of individual pieces. Arms, feet, noses, shins, eyebrows, breasts, clothes - everything can be mixed and matched, like a space-age Mr. Potato Head. This is obviously more difficult to set up in the short term, but it allows us to quickly create expressive, high-quality character busts that hopefully don't look &lt;em&gt;too&lt;/em&gt; creepily alike.&lt;/p&gt;

&lt;p&gt;Our artist has a particular creative process that I make no claim of understanding, but the important thing here is the file structure, which looks like this:&lt;/p&gt;

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

&lt;p&gt;For Photoshop neophytes (like myself): the folder-looking things are exactly that, and are called 'groups'. Groups can be infinitely nested. The leaf nodes of this tree structure are 'layers', and they contain image data. This could include vector shapes, text, or other effects, but for our purposes we can assume it's raster (bitmap) data.&lt;/p&gt;

&lt;p&gt;Each of these serves a different purpose: the lowest level layers are where the actual work gets done - so, we might have a layer for some line art, then another one for some shading. Directly above that (usually!) we have a group that collects all the layers for one 'piece', like 'SS_Ears_Kobold_Front_1'. Above that, there are groups used for organisation - gathering together the Kobold ears, all the ears, all the head pieces and so on. This allows the artist to hide the pieces that aren't relevant while working, but making them visible when he needs to check that they fit with the other adjacent pieces. There are also layers and groups used as references, temporary sketches, you get the idea.&lt;/p&gt;

&lt;p&gt;For practical reasons, the artist splits his work into multiple PSD (Photoshop Document) files which, as we'll see later, is probably a good thing.&lt;/p&gt;

&lt;p&gt;So! How do we get &lt;em&gt;that&lt;/em&gt; into a format we can use in Unity?&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt #1: keep it simple, stupid.
&lt;/h3&gt;

&lt;p&gt;The solution we first jumped to was the one built in to Photoshop: "Quick export layers as PNG". The artist need only individually mouse-select the &lt;u&gt;&lt;strong&gt;&lt;em&gt;434&lt;/em&gt;&lt;/strong&gt;&lt;/u&gt; groups corresponding to the pieces, hit the aforementioned option, and generate a load of images like this:&lt;/p&gt;

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

&lt;p&gt;And nicely cropped as well, how thoughtful! So I can just drop them all into Unity, and...&lt;/p&gt;

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

&lt;p&gt;Hmm. Okay, maybe I can make this work...&lt;/p&gt;

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

&lt;p&gt;Yeah.&lt;/p&gt;

&lt;p&gt;In hindsight, I spent an embarrassingly high number of hours, in and out of voice calls with the artist, trying to pixel-perfectly align each piece with some 433 others, before coming to the eminently sensible conclusion that this would not do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #2: Not enough RAM
&lt;/h2&gt;

&lt;p&gt;After looking into a few possible solutions, including things like &lt;a href="https://community.adobe.com/t5/photoshop-ecosystem-discussions/export-without-crop-trim/td-p/9914373" rel="noopener noreferrer"&gt;adding a nearly-transparent pixel in two corners of every layer to trick it into not cropping the images&lt;/a&gt;, the artist found a plugin for Photoshop that provided a slightly more configurable PNG exporter. The new exports would all be identical in size, matching the size of the canvas, with the graphic correctly positioned within it. The downside: you have to merge (combine into a single layer) each of the 434 groups before export - a process that takes several times as many clicks as the previous one. Safe to say, he was &lt;em&gt;not&lt;/em&gt; pleased.&lt;/p&gt;

&lt;p&gt;I, however, was ecstatic!&lt;/p&gt;

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

&lt;p&gt;...at least until I tried to shift-select all the images in Unity. After &lt;em&gt;minutes&lt;/em&gt; of waiting for the unresponsive editor, my PC locked up, having completely run out of memory.&lt;/p&gt;

&lt;p&gt;The problem is the sheer size of the images: 3500x5200 (enough to stay sharp on a 4k monitor). The size on disk isn't much changed with the addition of all this empty space--PNG provides pretty good lossless compression after all--but in Unity (both in the editor, and at runtime) things aren't so simple.&lt;/p&gt;

&lt;p&gt;In order to display a texture on screen, a desktop GPU requires it to be ultimately stored in one of a few different formats. For RGBA images like ours, we've got essentially two options: raw, uncompressed data at 32 bits per pixel, or the &lt;a href="https://en.wikipedia.org/wiki/S3_Texture_Compression" rel="noopener noreferrer"&gt;DXT5&lt;/a&gt; fixed-rate compression at 8 bits per pixel. Since blank pixels now cost just the same as filled ones, it adds up to 70MiB or 17MiB for a single image. All together it's 30GiB or 7.5GiB, and that could be for both system and GPU memory depending on the exact operation. The cost when selecting assets in the editor is probably twice that due to internal buffering.&lt;/p&gt;

&lt;p&gt;As an aside, &lt;a href="https://github.com/BinomialLLC/crunch" rel="noopener noreferrer"&gt;crunch compression&lt;/a&gt; is a bit of a red herring here. While it does reduce the size on disk, it needs to be expanded back to regular DXT when it's first displayed, so it won't solve our memory issues by itself.&lt;/p&gt;

&lt;p&gt;Now as much I love wasting my development headspace with caveats like "Don't ever touch Ctrl+A", this wouldn't do for the eventual players of the game, who would need something like a 2080 Ti in order to run our 2D RPG. If you're familiar with Unity you've probably just yelled the solution at the screen: sprite atlases! With one of these, we can pack all our sprites together into as few textures as possible, cropping out the empty space and even nestling them in each others' concave spaces, while preserving their positional data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ny89sf7hx5j9xu34qmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ny89sf7hx5j9xu34qmc.png" alt="Example sprite atlas, in case you didn't know."&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;A tip: you can drag a folder into the 'packable objects' list, and Unity will recursively add all the sprites within it to the atlas. Saves a lot of clicking and dragging!&lt;/p&gt;

&lt;p&gt;After adding the folder, I hit the "Pack Preview" button, and--you guessed it--got another out of memory crash. Perhaps I could create &lt;em&gt;multiple&lt;/em&gt; atlases, limited to a few dozen pieces each, but that would compromise memory efficiency, download size, and draw call count, all because I insisted on having a load of empty space around each sprite. And the problem would only get worse; we projected our final sprite count to be at &lt;em&gt;least&lt;/em&gt; 10 times what we currently have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #3: Process and reprocess.
&lt;/h2&gt;

&lt;p&gt;So we can't have cropped sprites because we lose the positional data, and we can't have uncropped sprites due to memory issues. But since the only thing the empty space provides us is an offset coordinate, perhaps we can extract this information from the raw images, and store them cropped in the project?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MenuItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tools/Trim sprites"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TrimSprites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// For each selected Texture asset...&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;textures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AssetDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAssetPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextureImporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;AssetImporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAtPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Disable compression, and make it 'readable';&lt;/span&gt;
        &lt;span class="c1"&gt;// this allows us to get a pointer to its data later on.&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isReadable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxTextureSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textureCompression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextureImporterCompression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uncompressed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Re-import the asset.&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAndReimport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Find the bounds of the graphic by looking for non-transparent pixels&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetNonTransparentBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;Vector2Int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="c1"&gt;// Calculate the new sprite pivot based on the computed bounds&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sourcePivot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spritePivot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sourcePivotPixels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sourcePivot&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spritePivot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourcePivotPixels&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Copy the graphic to a new, correctly-sized texture&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ARGB32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Write the texture to the original file path in the PNG format&lt;/span&gt;
            &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EncodeToPNG&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Undo the previous changes to the import settings&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isReadable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Re-import the asset, again.&lt;/span&gt;
        &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAndReimport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These new cropped, transformed sprites will happily pack into an atlas, while staying correctly aligned, so... yay?&lt;/p&gt;

&lt;p&gt;Well as you might imagine, this import/calculate/re-import process isn't exactly quick, and if the assets need updating we'd need to reset the pivot point and re-run the process. Plus, if we needed recover a layered PSD file from these images, it's more difficult to do so (though admittedly not impossible), and after all of this we still don't have the group structure of the original file. It "works", but surely, &lt;em&gt;surely&lt;/em&gt;, there's a more sustainable solution out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #4: Just use PSD bro!
&lt;/h2&gt;

&lt;p&gt;In the past, Unity had very poor support for layered image files, at best flattening the entire image into a single texture. This is rapidly changing, however, with the addition of the &lt;a href="https://docs.unity3d.com/Packages/com.unity.2d.psdimporter@7.0/manual/index.html" rel="noopener noreferrer"&gt;2D PSD Importer&lt;/a&gt; package. This adds a &lt;a href="https://docs.unity3d.com/Manual/ScriptedImporters.html" rel="noopener noreferrer"&gt;scripted importer&lt;/a&gt; which takes the original image, extracts all the layers, automatically crops and packs them into an atlas (not as efficiently as a regular sprite atlas, but good enough to save on memory use in the editor!), while &lt;em&gt;keeping&lt;/em&gt; the group structure. You can even share skeleton rigs between different sprite groups, and (in the beta version) individually enable or disable layers in the import settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fforum.unity.com%2Fattachments%2Flayers-png.970014%2F" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fforum.unity.com%2Fattachments%2Flayers-png.970014%2F" alt="Unity PSD importer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The artist, however, was sceptical. In order to get sprites for the pieces (instead of each art layer) he would still need to manually merge all the piece layer groups like he does now (the importer can sort of do this, but it's not very flexible), but with the added downside of having to upload a much larger file: the uncropped PNGs total about 20MiB, where the PSD was around 250!&lt;/p&gt;

&lt;p&gt;A bizarre limitation of scripted importers is that's impossible to handle any file extension (like &lt;code&gt;.psd&lt;/code&gt;) that Unity handles natively - even if the author of said importer is Unity themselves. Thus, the 2D PSD importer actually imports PS&lt;u&gt;B&lt;/u&gt; files - a very similar, but much less well-supported format. Before you send the devs any strongly-worded letters though, you can simply rename your &lt;code&gt;.psd&lt;/code&gt; files to &lt;code&gt;.psb&lt;/code&gt; and it'll work fine (a feature that remains undocumented at the time of writing, naturally).&lt;/p&gt;

&lt;p&gt;I persuaded the artist to send me his work file, and, with reckless curiosity, dropped it into Unity, which spun on the importer for about half an hour before crashing (probably due to out of memory, but I was disinclined to confirm this by trying again). Given the sheer number of art layers I'm not too surprised, but in any case I'd have to rule out postprocessing the file in Unity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #5: In a tiff.
&lt;/h2&gt;

&lt;p&gt;As much as Adobe might pretend otherwise, PSD isn't the only layered raster format in existence. The &lt;a href="https://en.wikipedia.org/wiki/TIFF" rel="noopener noreferrer"&gt;TIFF&lt;/a&gt; &lt;a href="https://en.wikipedia.org/wiki/RAS_syndrome" rel="noopener noreferrer"&gt;format&lt;/a&gt; supports multiple layers (called 'pages' or 'directories'), and both Photoshop and GIMP can save a TIFF file that they claim preserves layer information.&lt;/p&gt;

&lt;p&gt;Since I'm a cheap bastard I don't have a Photoshop license, so I used GIMP to start with. Exporting to TIFF gives you some extra settings, which I filled out like so:&lt;/p&gt;

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

&lt;p&gt;The whole process was rather quick, just a few seconds. I then opened the exported file, and got some more settings:&lt;/p&gt;

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

&lt;p&gt;And here's the layer structure that resulted:&lt;/p&gt;

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

&lt;p&gt;So, it looks like the TIFF export process had individually merged each top-level layer group. This was... not as helpful as I'd hoped, but it's something! The artist would need to un-structure his work, moving each piece group to the top level, but I theorised that it would be much faster than manually merging and selecting each of them.&lt;/p&gt;

&lt;p&gt;Of course, Unity won't handle these layered TIFFs in a useful way, so I had to make a scripted importer of my own!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Remember: Unity won't let us handle '.tif' or '.tiff'!&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ScriptedImporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tif2"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyTiffImporter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ScriptedImporter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnImportAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AssetImportContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This uses BitMiracle.LibTiff.NET from nuget&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tiff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assetPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;textures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pivots&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxHeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// For each 'page' within the TIFF file...&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Get some metadata (width, height, name)&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IMAGEWIDTH&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToInt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IMAGELENGTH&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToInt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                &lt;span class="n"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;maxHeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAGENAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No page name"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAGENAME&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                &lt;span class="c1"&gt;// Read the 'raster' (pixel data)&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;raster&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadRGBAImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetNonTransparentBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// Skip all-transparent pages&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="c1"&gt;// Calculate the page's X/Y offset&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;xres&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XRESOLUTION&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;yres&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YRESOLUTION&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;xpos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XPOSITION&lt;/span&gt;&lt;span class="p"&gt;)?[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ypos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;YPOSITION&lt;/span&gt;&lt;span class="p"&gt;)?[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pageBounds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RectInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;xpos&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;xres&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;ypos&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;yres&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;height&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// Calculate the pivot based on the page's position and calculated bounds&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;srcPivot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)*&lt;/span&gt;&lt;span class="n"&gt;pageBounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)*&lt;/span&gt;&lt;span class="n"&gt;pageBounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)/&lt;/span&gt;&lt;span class="n"&gt;pageBounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pivot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;srcPivot&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pageBounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;pivots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// Create a new texture for the cropped image&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RGBA32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;alphaIsTransparency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
                &lt;span class="p"&gt;};&lt;/span&gt;

                &lt;span class="c1"&gt;// Copy the pixels from the raster into the texture&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetPixelData&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// data.CopyFrom(raster);&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="n"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// ReadDirectory returns false when there are no more pages to read&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadDirectory&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="c1"&gt;// Create a new texture for the combined image&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;atlas&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DXT5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;alphaIsTransparency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="c1"&gt;// This function packs the textures somewhat loosely, but good enough for now!&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PackTextures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;EditorUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompressTexture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DXT5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextureCompressionQuality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Best&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddObjectToAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"atlas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetMainObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Add the Sprite to the asset&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sprite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;rects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;TransformSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atlas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                    &lt;span class="n"&gt;pivots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddObjectToAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sprite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RectInt&lt;/span&gt; &lt;span class="nf"&gt;GetNonTransparentBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raster&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;maxX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;maxY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RectInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxX&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxY&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's rough, but it sort of works!&lt;/p&gt;

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

&lt;p&gt;More importantly, though: how easy would it be for our artist to create a valid file? At first it seemed the answer was 'extremely' - as simple as Save As -&amp;gt; TIFF - but when I opened the file in GIMP it seemed to only have a single layer.&lt;/p&gt;

&lt;p&gt;Opening the file in the very retro-looking &lt;a href="http://www.thesilentfish.de/software/archive/itm_TiffInspec103.en.html" rel="noopener noreferrer"&gt;TIFF Inspector&lt;/a&gt; gave me this:&lt;/p&gt;

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

&lt;p&gt;One directory (i.e. layer), and a lot of 'privately defined' tags. Using &lt;a href="https://www.awaresystems.be/imaging/tiff/tifftags/private.html" rel="noopener noreferrer"&gt;a very helpful reference&lt;/a&gt;, we find that most of these tags are irrelevant--colour profiles, thumbnail data and suchlike--but tag 37724 seems to have some proprietary Photoshop-related data, which is corroborated by the &lt;a href="https://www.alternatiff.com/resources/TIFFphotoshop.pdf" rel="noopener noreferrer"&gt;Photoshop TIFF spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hang on, Photoshop TIFF spec? Yeah, the TIFFs that Photoshop creates are, for our purposes, totally proprietary, so it's essentially a PSD file but even less well supported. Great! Apparently &lt;a href="https://imagemagick.org/script/formats.php" rel="noopener noreferrer"&gt;ImageMagick&lt;/a&gt; has support for getting the layer data out of this 'variant', so if you have a bunch of files in the format already, you can still make use of them.&lt;/p&gt;

&lt;p&gt;I could get the artist to open his work file in GIMP and go through the export process there, but by this point it seemed like a bit of a hassle for not much benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #6: Can't I just ZIP a bunch of PNGs together or something?
&lt;/h2&gt;

&lt;p&gt;Before I resorted to making my own format out of chewing gum and string, I thought I'd have a quick browse of the other layered raster formats out there, just to see if there were any other options - and wouldn't you know it, there are! The &lt;a href="https://www.openraster.org/baseline/baseline.html" rel="noopener noreferrer"&gt;OpenRaster&lt;/a&gt; format (&lt;code&gt;.ora&lt;/code&gt;) is an open standard, is supported in GIMP, and is &lt;em&gt;literally a ZIP of PNGs&lt;/em&gt;, with an XML file describing the layer structure - groups and all:&lt;/p&gt;

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

&lt;p&gt;So it &lt;em&gt;appeared&lt;/em&gt; (foreshadowing!) that OpenRaster was a good candidate for our 'final' asset format. However, we still had the problem of how to go from our many-layered PSD work file to a few-layered OpenRaster file. Merging all the layers was still a manual process, and I have to confess I wouldn't be happy if I had to do several thousand clicks just to get my art in a nice format for the programmers.&lt;/p&gt;

&lt;p&gt;So I made a plugin!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scheme"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;merge-and-optimize-recursive&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;for-each&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="nv"&gt;merged-layer&lt;/span&gt; &lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;TRUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-item-is-group&lt;/span&gt; &lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;children&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;vector-&amp;gt;list&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cadr&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-item-get-children&lt;/span&gt; &lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;; If any children are not groups, merge the item&lt;/span&gt;
            &lt;span class="c1"&gt;; Otherwise, recurse.&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;foldr&lt;/span&gt; &lt;span class="nv"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;gimp-item-is-group&lt;/span&gt; &lt;span class="nv"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;set!&lt;/span&gt; &lt;span class="nv"&gt;merged-layer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-merge-layer-group&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;merge-and-optimize-recursive&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;; Auto-crop the (possibly merged) layer&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;FALSE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-item-is-group&lt;/span&gt; &lt;span class="nv"&gt;merged-layer&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-set-active-layer&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;merged-layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;plug-in-autocrop-layer&lt;/span&gt; &lt;span class="nv"&gt;RUN-NONINTERACTIVE&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;merged-layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;script-fu-merge-and-optimize&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-undo-group-start&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;; The final assets will be in 8-bit RGBA, so convert the image to that if needed.&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;PRECISION-U8-GAMMA&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-get-precision&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-convert-precision&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;PRECISION-U8-GAMMA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;merge-and-optimize-recursive&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;vector-&amp;gt;list&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cadr&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-get-layers&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gimp-image-undo-group-end&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;script-fu-register&lt;/span&gt;
    &lt;span class="s"&gt;"script-fu-merge-and-optimize"&lt;/span&gt;
    &lt;span class="s"&gt;"Merge layer groups and optimize"&lt;/span&gt;
    &lt;span class="s"&gt;"Merge layer groups and optimize"&lt;/span&gt;
    &lt;span class="s"&gt;"Hamish Milne"&lt;/span&gt;
    &lt;span class="s"&gt;"Hamish Milne"&lt;/span&gt;
    &lt;span class="s"&gt;"2021"&lt;/span&gt;
    &lt;span class="s"&gt;"*"&lt;/span&gt;
    &lt;span class="nv"&gt;SF-IMAGE&lt;/span&gt; &lt;span class="s"&gt;"Image"&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nv"&gt;SF-DRAWABLE&lt;/span&gt; &lt;span class="s"&gt;"Layer"&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;script-fu-menu-register&lt;/span&gt; &lt;span class="s"&gt;"script-fu-merge-and-optimize"&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;Image&amp;gt;/Image"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... Or, more specifically a '&lt;a href="https://docs.gimp.org/en/gimp-using-script-fu-tutorial.html" rel="noopener noreferrer"&gt;Script-Fu&lt;/a&gt; Script'. The -ahem- 'code' above is &lt;a href="https://www.gnu.org/software/mit-scheme/" rel="noopener noreferrer"&gt;Scheme&lt;/a&gt;, specifically &lt;a href="http://tinyscheme.sourceforge.net/home.html" rel="noopener noreferrer"&gt;TinyScheme&lt;/a&gt;, GIMP's scripting runtime of choice. It's also possible to use Python 2.7 (ugh), or compile an executable from scratch, but Scheme is a lot less painful for simple scripts like this one.&lt;/p&gt;

&lt;p&gt;There's a few things to note here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The input files will generally use high-precision colours for more accurate composition while editing. Before everything, I change the image precision to 8-bit gamma encoding, since this is what will ultimately be used by Unity when importing; skipping this step will result in needlessly large output files.&lt;/li&gt;
&lt;li&gt;My heuristic for whether to merge layers is somewhat specific to our pipeline. I merge the groups directly above the leaf layers and leave the rest alone.&lt;/li&gt;
&lt;li&gt;I run the Auto Crop function on every layer, which cuts it down to the smallest rectangle that encloses the graphic. This obsoletes my Unity-based solution, and naturally makes the output files smaller still.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have to say, it was certainly satisfying watching layer groups flick down the screen as the script did its thing. &lt;a href="https://i.imgur.com/etcWoQA.gif" rel="noopener noreferrer"&gt;If you're sensitive to flashing images, though, I recommend looking away from the screen for a bit...&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On exporting to the OpenRaster format, everything seemed to work! I had, effectively, a ZIP of all the cropped layers, in PNG format, along with the layer structure. That's just about everything I'd been looking for, right?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Right?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Well... For starters, Unity has &lt;em&gt;zero&lt;/em&gt; support for OpenRaster, so I'd need to make another scripted importer. Not too difficult, since the format's so simple, but I couldn't help feeling some chagrin that I'd have to essentially re-implement all the features of the Unity PSD importer, just in a much more janky way.&lt;/p&gt;

&lt;p&gt;Also, it took GIMP several minutes to do the export. On a more reasonably-specced machine, and with a larger file, it might push half an hour. I don't know exactly &lt;em&gt;why&lt;/em&gt; this is the case, when exporting to TIFF or PSD takes seconds, but it's probably to do with the OpenRaster exporter being a Python plugin, where those other ones are built in to the main program.&lt;/p&gt;

&lt;p&gt;Man, it'd be cool if we could just use PSD, huh?&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt #7: Seriously, just use PSD.
&lt;/h2&gt;

&lt;p&gt;The main issue we had with using PSD wasn't the format itself, it was the total effort required to prepare the file for export. With the plugin I'd made, that just became a non-issue; I'd reduced the preparation time to almost nothing, and we could export in whatever format we pleased. Why wouldn't PSD do?&lt;/p&gt;

&lt;p&gt;In fact, with all my optimizations, the PSD file exported after running the script was only about 20% larger than the equivalent OpenRaster file, and about half the size of all the un-cropped PNGs. The PSD shrunk from 250MiB to a little over 10. And this time, Unity didn't crash on the importer!&lt;/p&gt;

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

&lt;p&gt;The only major caveat with this approach is the size of the intermediary atlas. Unlike regular sprite atlases, the PSD importer will create one big atlas texture per file. Unity (and indeed, most GPUs) has a maximum texture size of 16k square, even in the editor. If your sprites don't fit, they'll be shrunk until they do, and the sprite atlas won't be able to un-shrink them later on. So if the intermediary atlas looks pretty full, you might want to break up the PSD into smaller files.&lt;/p&gt;

&lt;p&gt;Another thing to watch out for is this:&lt;/p&gt;

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

&lt;p&gt;Neither GIMP nor Unity's PSD importer will perfectly handle every Photoshop feature. The effect above is caused by a mask being incorrectly applied, so if your layers are doing anything beyond being linearly composed together, it's a good idea to rasterize them just before you export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: What have we learned?
&lt;/h2&gt;

&lt;p&gt;If you find yourself in the incredibly specific position of having a huge amount of structured, fixed-position sprites, created in Photoshop, that need to be imported into Unity, you've got a few options:&lt;/p&gt;

&lt;p&gt;Quick export layers as PNGs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Quick and simple&lt;/li&gt;
&lt;li&gt;Pro: Small file size (thanks to the cropping)&lt;/li&gt;
&lt;li&gt;Con: You lose the positional data and group structure (thanks to the cropping)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;a href="https://www.fnordware.com/superpng/" rel="noopener noreferrer"&gt;SuperPNG&lt;/a&gt; or similar to export un-cropped PNGs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: No special software needed to process the output&lt;/li&gt;
&lt;li&gt;Pro: Keeps position data&lt;/li&gt;
&lt;li&gt;Con: Need to manually merge each sprite group&lt;/li&gt;
&lt;li&gt;Con: No group structure&lt;/li&gt;
&lt;li&gt;Con: Requires post-processing in Unity&lt;/li&gt;
&lt;li&gt;Con: Very easy to accidentally run out of memory in the editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the PSD work file directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Easiest of the bunch for the artist&lt;/li&gt;
&lt;li&gt;Pro: You use exactly the same file, making it easy to stay in sync&lt;/li&gt;
&lt;li&gt;Pro: Keeps the position data and group structure&lt;/li&gt;
&lt;li&gt;Pro: Importer made by Unity, will get new features over time&lt;/li&gt;
&lt;li&gt;Con: &lt;em&gt;Massive&lt;/em&gt; file size&lt;/li&gt;
&lt;li&gt;Con: Importing takes forever and might crash if the file is too big&lt;/li&gt;
&lt;li&gt;Con: If you have a weird layer structure, make use of masks, smart objects etc. you might still need to pre-process the file for Unity to display it correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bring each sprite's group to the top level, then export to TIFF in GIMP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Fewer clicks than merging each sprite group individually&lt;/li&gt;
&lt;li&gt;Pro: Fairly small file size&lt;/li&gt;
&lt;li&gt;Pro: Keeps position data&lt;/li&gt;
&lt;li&gt;Con: No group structure&lt;/li&gt;
&lt;li&gt;Con: Requires a custom scripted importer&lt;/li&gt;
&lt;li&gt;Con: Multi-page TIFFs not well supported&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a GIMP script to optimize the file, then export to OpenRaster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Smallest file size of the lot&lt;/li&gt;
&lt;li&gt;Pro: Automatic processing saves a lot of time&lt;/li&gt;
&lt;li&gt;Pro: Simple format, easy to parse and use elsewhere&lt;/li&gt;
&lt;li&gt;Pro: Keeps the position data and group structure&lt;/li&gt;
&lt;li&gt;Con: Requires a custom scripted importer&lt;/li&gt;
&lt;li&gt;Con: Format not well supported&lt;/li&gt;
&lt;li&gt;Con: Takes a long time to export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a GIMP script to optimize the file, then export to PSD (our solution of choice):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Fairly small file size&lt;/li&gt;
&lt;li&gt;Pro: Automatic processing saves a lot of time; quick to export&lt;/li&gt;
&lt;li&gt;Pro: Keeps the position data and group structure&lt;/li&gt;
&lt;li&gt;Pro: Importer made by Unity, will get new features over time&lt;/li&gt;
&lt;li&gt;Con: Need to rasterize masks etc. for Unity to display it correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap: How we do it
&lt;/h2&gt;

&lt;p&gt;For reference (or if you've just skipped to the end), here's our full 2D pipeline, step by step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Artist setup:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Install Photoshop (obviously)&lt;/li&gt;
&lt;li&gt;Install GIMP&lt;/li&gt;
&lt;li&gt;Copy the Scheme code above to &lt;code&gt;%APPDATA%\GIMP\&amp;lt;GIMP version&amp;gt;\scripts\script-fu-merge-and-optimize.scm&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;While creating:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Structure the layers as you like, but make sure that the group for each sprite only contains layers, and not other groups.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;To export:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Delete (not hide!) any layers that you don't want in the final output (references and so on)&lt;/li&gt;
&lt;li&gt;If you're using masks, smart objects, patterns etc., rasterize and/or merge the layers as appropriate so that only simple layers remain&lt;/li&gt;
&lt;li&gt;Open the PSD file in GIMP&lt;/li&gt;
&lt;li&gt;Run the script by going to 'Image -&amp;gt; Merge layer groups and optimize'&lt;/li&gt;
&lt;li&gt;NB, it's not necessary to make all the layers visible at this stage&lt;/li&gt;
&lt;li&gt;Check the results, then export the file as PSD&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;To import:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Install the "2D PSD Importer" package, if it's missing&lt;/li&gt;
&lt;li&gt;Change the image file's extension to &lt;code&gt;.psb&lt;/code&gt;, and copy it into the project&lt;/li&gt;
&lt;li&gt;Check the import settings - in particular, the texture size, hidden layers, and layer group options&lt;/li&gt;
&lt;li&gt;Ensure your sprites are added to an atlas before building&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And... that's it! After a lot of trial and error, we've got what I think is a pretty powerful and robust asset pipeline, which hopefully won't make our artist pull his hair out every time he needs to do an export.&lt;/p&gt;

&lt;p&gt;All the code in this post is &lt;a href="https://choosealicense.com/licenses/isc/" rel="noopener noreferrer"&gt;ISC&lt;/a&gt; licensed. Feel free to use it if you find it useful!&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
