Adobe CEP ExtendScript Hot Module Reloading
The Context
As part of a current project, I’m working on an Adobe CEP extension to augment the behaviour of Adobe Illustrator to add new features.
As a part of this we use ExtendScript to interface with the host application.
When writing these scripts, you can include in the manifest (adjacent to your
MainPath XML attribute) a declaration for a script <ScriptPath>./jsx/index.js</ScriptPath> to be run on app startup. Here you can declare functions (in ES3 syntax) to call later on in the browser in your app like so:// <extension-root>/jsx/index.js function greet(person) { alert("Hello " + person + "!"); }
const csInterface = new CSInterface() csInterface.evalScript(`greet('${name}')`);
Which will present the user an alert saying
Hello James!Note: CEP runs extensions of both
.js and .jsx - here I use .js (as ExtendScript is ES3 compliant with some additional globals available in the scope).The Problem
The Adobe host application (e.g. Illustrator) loads this script path on start of the application, which is fine during normal usage of the application, but in development mode when iterating on non-trivial ExtendScript functions, having to close and open Illustrator to see your changes reflected becomes extremely frustrating very quickly and slows down development.
The Solution
We have a generic bridge between TypeScript and ExtendScript
evalIllustratorScript which wraps. The idea is, when running in development mode (not in production), re-evaluate the script file which redeclares the functions afresh before your script calls those functions.We don’t want to do this in production due to the performance cost of re-running the script file on each call to ExtendScript. This means:
- In production:
- Keep the
<ScriptPath>tag in the built manifest - Evaluate
index.jsonce, on start-up of the plugin
- In dev mode:
- Remove the
<ScriptPath>as this interrupts the behaviour of$.evalFile, which will be required for the implementation - Evaluate
index.jsevery time we need to call an ExtendScript function
In practice, this means we need to build our manifest dynamically for dev mode vs production.
This approach means we don’t need to restart Illustrator on every change to the index file. If there is some build system which outputs the
index.js (e.g. you’re using TypeScript), then you will need to run the build before seeing changes reflected. You can make changes truly instant by adding some sort of watch mode to your build system which watches for changes.The Code
enum SystemPath { // ... EXTENSION = "extension", } // ... // Added to the evalIllustratorScript to run before each function invocation if (process.env.NODE_ENV === "development") { const extensionRoot = csInterface.getSystemPath(SystemPath.EXTENSION); const bundlePath = `${extensionRoot}/jsx/index.js`; csInterface.evalScript(`$.evalFile('${bundlePath}')`); } csInterface.evalScript(script);
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- /************************************************************************* * ADOBE CONFIDENTIAL * ___________________ * * Copyright 2014 Adobe Inc. * All Rights Reserved. * * NOTICE: Adobe permits you to use, modify, and distribute this file in * accordance with the terms of the Adobe license agreement accompanying * it. If you have received this file from a source other than Adobe, * then your use, modification, or distribution of it requires the prior * written permission of Adobe. **************************************************************************/ --> <ExtensionManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ExtensionBundleId="com.myplugin" ExtensionBundleName="myplugin" ExtensionBundleVersion="1.0.0" Version="12.0"> <ExtensionList> <Extension Id="com.myplugin.extension" Version="1.0"/> </ExtensionList> <ExecutionEnvironment> <HostList> <Host Name="ILST" Version="[25.0,99.9]" /> </HostList> <LocaleList> <Locale Code="All"/> </LocaleList> <RequiredRuntimeList> <RequiredRuntime Name="CSXS" Version="12.0" /> </RequiredRuntimeList> </ExecutionEnvironment> <DispatchInfoList> <Extension Id="com.myplugin.extension"> <DispatchInfo> <Resources> <MainPath>./index.html</MainPath> <!-- SCRIPT_PATH_PLACEHOLDER --> </Resources> <Lifecycle> <AutoVisible>true</AutoVisible> <StartOn> <Event>applicationActivate</Event> </StartOn> </Lifecycle> <UI> <Type>Panel</Type> <Menu>My Plugin</Menu> <Geometry> <Size> <Height>700</Height> <Width>450</Width> </Size> </Geometry> <Icons> <Icon Type="Normal">./images/icon_light.png</Icon> <Icon Type="RollOver">./images/icon_light@2x.png</Icon> <Icon Type="DarkNormal">./images/icon_dark.png</Icon> <Icon Type="DarkRollOver">./images/icon_dark@2x.png</Icon> </Icons> </UI> </DispatchInfo> </Extension> </DispatchInfoList> </ExtensionManifest>
# scripts/process-manifest.sh #!/bin/sh set -e # (dev|production|staging|local-bundle) ENVIRONMENT="${1:-${PLUGIN_ENVIRONMENT:-staging}}" SOURCE_MANIFEST="${2:-./apps/plugin/src/CSXS/manifest.xml}" OUTPUT_MANIFEST="${3:-./build/plugin/CSXS/manifest.xml}" mkdir -p "$(dirname "$OUTPUT_MANIFEST")" TEMP_FILE=$(mktemp) if [ "$ENVIRONMENT" = "dev" ]; then # Commented out in dev mode sed 's|<!-- SCRIPT_PATH_PLACEHOLDER -->|<!-- <ScriptPath>./jsx/index.js</ScriptPath> -->|' \ "$SOURCE_MANIFEST" > "$TEMP_FILE" else sed 's|<!-- SCRIPT_PATH_PLACEHOLDER -->|<ScriptPath>./jsx/index.js</ScriptPath>|' \ "$SOURCE_MANIFEST" > "$TEMP_FILE" fi rm "$TEMP_FILE"