sketch-plugin-types
Version:
TypeScript type definitions for the Sketch app plugin API (sketch/dom, sketch/ui, sketch/settings, sketch/async, sketch/data-supplier) and the Action API.
172 lines (131 loc) • 7.66 kB
Markdown
# Building with skpm
> **Status note (2026-04).** `@skpm/builder` is in maintenance mode: the
> latest release is `0.9.5` published **2023-10-10**, the parent repo's
> last commit is the same day, and the bundler still targets webpack 4.
> The runtime pieces Sketch needs — `sketch-module-web-view`,
> `sketch-polyfill-fetch` — are still patched actively, but the build
> tooling is effectively frozen. It works; it just isn't evolving.
>
> For a **greenfield** plugin, consider bypassing skpm entirely and
> building directly onto the `.sketchplugin` bundle layout with `tsc` +
> a small bundle script (~20 lines) or `esbuild`. See
> [`examples/hello-world/`](../examples/hello-world/) for the minimal
> template; several production plugins in the wild use this approach.
> You keep `sketch-plugin-types` either way.
>
> If you already have a skpm project, or you want `skpm watch` / `skpm
> publish` ergonomics, the rest of this guide still applies.
[`skpm`](https://github.com/skpm/skpm) is the standard Sketch plugin toolchain. It scaffolds a project, bundles your code (webpack + babel), installs the plugin into Sketch, watches files on save, and publishes releases.
## Setup
Install skpm once, globally:
```sh
npm install -g skpm
```
Create a new plugin:
```sh
skpm create my-plugin
cd my-plugin
npm install
```
You get a plugin folder:
```
my-plugin/
src/
manifest.json source manifest
my-command.js one file per command
assets/ icons and other resources
package.json has an "skpm" field pointing at src/manifest.json
```
## Add TypeScript
```sh
npm install --save-dev typescript sketch-plugin-types
```
Rename your source files from `.js` to `.ts`, drop an `env.d.ts` into `src/`:
```ts
// src/env.d.ts
/// <reference types="sketch-plugin-types" />
import 'sketch-plugin-types/globals';
```
`src/manifest.json` keeps pointing at the `.js` output (skpm writes `.js` regardless of source extension):
```json
{
"commands": [
{ "script": "my-command.js", "handler": "onRun", "...": "..." }
]
}
```
In principle `@skpm/builder` hands `.ts` sources to Babel and they compile without extra config. In practice, on `@skpm/builder@0.9.5` TypeScript is not always picked up, and the failure mode is silent — the `.ts` file is either ignored or emitted as-is and crashes in Sketch. If you hit that, drop a `webpack.skpm.config.js` next to `package.json` and wire `ts-loader` explicitly:
```js
// webpack.skpm.config.js
module.exports = function (config, _isPluginCommand) {
config.module = config.module || {};
config.module.rules = config.module.rules || [];
config.module.rules.push({
test: /\.tsx?$/,
exclude: /node_modules/,
use: [{ loader: 'ts-loader', options: { transpileOnly: true } }],
});
config.resolve = config.resolve || {};
config.resolve.extensions = [
'.ts', '.tsx',
...(config.resolve.extensions || ['.js', '.json']),
];
return config;
};
```
```sh
npm install --save-dev ts-loader@^8
```
`ts-loader@8` is the last line that supports the webpack 4 skpm ships. Do not upgrade to `ts-loader@9` — it requires webpack 5.
If you additionally import `webpack-merge`, use `merge()` and not `smart()` — the `smart` helper was removed and a transitive dep still references it under certain npm tree shapes.
## Run it
```sh
npm run build # one-off build
npm run watch # rebuild on save
npm run start # watch + reload Sketch on each build
```
The first build installs the plugin into `~/Library/Application Support/com.bohemiancoding.sketch3/Plugins/` as a symlink, so subsequent rebuilds are visible to Sketch right away.
## See the log
```sh
skpm log -f # tail the plugin log
```
## Publish a release
```sh
skpm login # paste a GitHub token once (repo scope)
skpm publish patch # bumps version, tags, creates GitHub release
```
skpm publishes to a GitHub release that Sketch can auto-update from if you set `appcast` in the manifest.
## Templates
```sh
skpm create my-plugin --template with-datasupplier
skpm create my-plugin --template with-webview
```
See [`skpm/skpm`](https://github.com/skpm/skpm) for the full template list.
## Polyfilled core modules
`@skpm/builder` bundles small polyfills for `buffer`, `path`, `crypto`, `stream`, `util`, `events`, `url`, and `assert` — enough to let Node-oriented deps run inside a Sketch plugin. Opt in to the types:
```ts
// src/env.d.ts
/// <reference types="sketch-plugin-types" />
import 'sketch-plugin-types/globals';
import 'sketch-plugin-types/skpm';
```
Then `import * as path from 'path'`, `const { Buffer } = require('buffer')`, etc. resolve with types. Do not install `@types/node` — skpm's polyfill surface is deliberately narrower than Node's, and `@types/node` will over-promise methods that blow up at runtime.
`fs` is intentionally absent — skpm does not polyfill it. Use `NSFileManager` (see [cocoa.md](./cocoa.md)).
## Not polyfilled — shim yourself
Sketch runs plugins in a bare JavaScriptCore context. A number of things most JS libraries assume on `globalThis` are *not* there and skpm does not provide them. The ones we see people hit most often:
- `structuredClone` — missing. Any library that clones state defensively (e.g. `dagre`, `immer` in certain paths) will throw `ReferenceError: Can't find variable: structuredClone`. Shim with `JSON.parse(JSON.stringify(…))` for JSON-safe graphs, or pull in the [`@ungap/structured-clone`](https://www.npmjs.com/package/@ungap/structured-clone) ponyfill for Date / Map / Set / typed arrays.
- `TextEncoder` / `TextDecoder` — missing. Convert through `NSString` + `NSData`:
```ts
const NSString = NSClassFromString('NSString');
const data = NSString.stringWithUTF8String_(input).dataUsingEncoding_(4); // NSUTF8StringEncoding
```
Or vendor a small pure-JS implementation (`fast-text-encoding`).
- `fetch` — **injected** by `@skpm/builder` via webpack's `ProvidePlugin` → [`sketch-polyfill-fetch`](https://github.com/skpm/sketch-polyfill-fetch) (wraps `NSURLSession`). The function works out of the box; no types ship here, so write `declare const fetch: (...args: any[]) => Promise<any>;` or narrow locally.
- `Request` / `Response` / `Headers` — **not native**. `sketch-polyfill-fetch` returns plain objects with `ok` / `status` / `headers` / `text()` / `json()` but is *not* a WHATWG implementation — `new Response(...)` / `new Headers(...)` / `new Request(...)` all throw. Anything that expects `instanceof Response` will break.
- `FormData` — injected by the same `ProvidePlugin`. Usable as a constructor.
- `URL` / `URLSearchParams` — `URL` is polyfilled via the `url` module, but the WHATWG globals are not injected on `globalThis`. Import what you need from `'url'`.
- `Promise` — present on modern Sketch, but there is no native microtask queue for long-running commands. If your command exits synchronously the promise resolver never fires. Use `Async.createFiber()` to keep the runtime alive (see [developer.sketch.com](https://developer.sketch.com/reference/api/#async)).
- `queueMicrotask` / `setImmediate` / process ticks — missing. Use `setTimeout(fn, 0)`.
- `crypto.subtle`, `crypto.getRandomValues` — the skpm `crypto` polyfill only covers `createHash` / `randomBytes`. For WebCrypto, bridge to `NSData` / `CCHmac` via Cocoa or vendor a JS implementation.
- `fs` — see above; use `NSFileManager`.
If a dependency crashes immediately with `ReferenceError: Can't find variable: X`, that's almost always one of these. Shim at the top of your entry script before the first `import`.