A modern starter for writing Adobe Extendscript
(yes, another one)
Writing Extendscript (Ecmascript 3) is rather annoying once you're used to modern Javascript. You expect to use array methods and common practices such as .env files, no-nonsense bundling, rebuilding on changes, etc.
Other starters don't actually transform modern Javascript, so you have to write helper functions for your beloved array methods which totally defeats the point of writing modern Javascript. You might polyfill them, but that pollutes the global scope (which is bad for everyone if you found a shitty implementation on Stack Overflow).
- Modern Javascript with ponyfills (see babel-preset-extendscript)
- Ecmascript modules for importing and exporting
- Fast bundling with esbuild (TODO: insert obligatory lightning bolt emoji)
- Rebundles on file changes
- Shortens release identifiers while keeping JSXBIN-safe whitespace
- Converts to binary with extendscript-debugger
- Wraps bundle in an IIFE to avoid global variables and expose Adobe's
thisObjfor dockable ScriptUI panels - Exposes explicit build constants to Javascript files
- Includes JSON automatically as a ponyfill
- Copies static files from
/static(with esbuild-copy-static-files) - Imports
?textsuffixed paths as strings - Imports icons as binary strings
- Includes the
msongz/songz-modulessubmodule for reusable After Effects helpers
- Duplicate
.env.exampleand remove the.exampleextension - Run
git submodule update --init --recursive - Run
npm install && npm startin your terminal - Run
/build/extender.jsxin your Adobe app of choice
npm ci
npm run check
npm startThis will start watching your source files and builds into the build folder.
Use npm run ci before submitting a change. It validates the repository, creates
a production build, and verifies every generated entry point and copied static
file. GitHub Actions runs the same command for pushes and pull requests.
npm run releaseThis will bundle and convert your source files to JSXBIN in the dist folder.
The bundler exposes only these documented compile-time constants:
DEVMODEwhenNODE_ENVisdevelopmentor not,PRODUCT_NAMEwhich isnamefrompackage.json,PRODUCT_DISPLAY_NAMEwhich isdisplayNamefrompackage.json,PRODUCT_VERSIONwhich isdisplayVersionorversionfrompackage.json,PRODUCT_DEVELOPERwhich isauthorfrompackage.json, andI18N_LOCALEwhich can override the detected host locale for localized UI strings
A local .env file can override the corresponding product constants. Other
process environment variables are intentionally not injected into source code,
which keeps builds deterministic and avoids replacing unrelated identifiers. Add
new compile-time constants explicitly to the define object in
scripts/build.js.
Every .js file in the root of the src/ folder is considered an entrypoint. Multiple entrypoints will result in multiple scripts being bundled separately, keeping the same name as their entrypoint. If there's a single entrypoint it will be renamed to name from package.json.
The build wraps each bundle with (function (thisObj) { ... })(this);, so entrypoints can pass thisObj into panel helpers. The example in src/main.js uses addWindow from songz-modules/ui.js to open as a dockable panel when installed from the ScriptUI Panels folder, while still falling back to a floating palette when run directly.
For larger After Effects tools, import shared helpers from songz-modules:
import { addWindow, show } from '../songz-modules/ui'src/modules/templateFrame.js provides a reusable ScriptUI shell inspired by segmentRender:
- colored outer border and themed content background
homeGroupandsettingsGroupviews with a footer Settings/Back toggle- footer version text, developer text, hover highlight, and click-to-open links
flashVersionInfo()for temporary status/version text overridesopenProjectPage()andopenDeveloperPage()helpers
The example src/main.js shows how future tools can put product controls in frame.homeGroup, settings controls in frame.settingsGroup, and then call frame.show().
Press F5 in VSCode to launch the debugger and you will be prompted for the host application. To avoid the prompt set the hostAppSpecifier in launch.json
You can't use breakpoints in your source files, because the Extendscript Debugger doesn't support source maps. Instead, use a debugger statement in your source files or set breakpoints in the bundled file (/build/{PRODUCT_NAME}.jsx).
To import JavaScript files as strings you can suffix the path with ?text and import them with a custom default name. This is useful when you have snippets that you want to import as strings. Having them as separate files allows you to format and lint them separately, use TypeScript definitions on them, etc. Note that they will be imported as-is and don't get transpiled. This works for any other text based files.
See src/main.js
Using icons in scriptUI is easiest when you add them to your source code. To do this you can import the icons directly as .png or .jpg. They are then transformed to a binary string that can be read by File.decode().
See src/main.js
You can import Node modules by simply using import xyz from 'xyz'. Note that they can't contain browser or Node APIs. Also note that quite some modern Javascript is not yet ponyfilled by babel-preset-extendscript.
The contents of /static will be copied to the outdir whenever you run the bundler. This is useful for icons, readme files, etc. Note that any changes in this folder will not be watched, so you need to run the bundler again or save a change in your source files.
Release builds shorten variable names but keep whitespace intact because JSXBIN can fail when an ExtendScript bundle is collapsed into one very long line. The syntax remains intact to avoid Extendscript errors.
Types for Adobe is part of the package, so you can simply use triple-slash directives to get the type definitions for your target app. Or create a tsconfig.json to define which types to use in the whole project.
See src/main.js
You should even be able to make it work with Typescript if that's your thing.
You can still write regular old Extendscript without the modern syntax.