├── .env.example ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts ├── binary.js ├── build.js ├── jsxbin.js └── text.js ├── src ├── icons │ ├── icon.jpg │ └── icon.png ├── main.js └── modules │ ├── expression.js │ └── utils.js └── static └── README.html /.env.example: -------------------------------------------------------------------------------- 1 | IM_IN_ENV=Env says hello 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ["https://www.paypal.com/donate/?hosted_button_id=RGCDAUP9P2DNQ"] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .DS_Store 5 | .serve 6 | .env 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "adobe.extendscript-debug" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Extendscript", 9 | "type": "extendscript-debug", 10 | "request": "launch", 11 | "script": "${workspaceFolder}/build/extender.jsx", 12 | // NOTE: Enable `hostAppSpecifier` to avoid being prompted 13 | // "hostAppSpecifier": "aftereffects-22.0", 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | - `Added` for new features 9 | - `Changed` for changes in existing functionality 10 | - `Deprecated` for soon-to-be removed features 11 | - `Removed` for now removed features 12 | - `Fixed` for any bug fixes 13 | - `Security` in case of vulnerabilities 14 | 15 | ## [0.0.10] - 2022-12-23 16 | 17 | ### Changed 18 | 19 | - Import as text with `?text` suffix instead of `.text.js` 20 | 21 | ## [0.0.9] - 2022-11-22 22 | 23 | ### Added 24 | 25 | - References to 'Types for Adobe' types 26 | 27 | ### Fixed 28 | 29 | - Cross platform npm scripts 30 | - Error handling in `jsxbin.js` 31 | - Script path in `launch.json` 32 | 33 | ## [0.0.8] - 2022-11-14 34 | 35 | ### Added 36 | 37 | - Import `.png` and `.jpg` icons as binary string 38 | 39 | ## [0.0.7] - 2022-11-08 40 | 41 | ### Added 42 | 43 | - Import JavaScript files as text 44 | 45 | ## [0.0.6] - 2022-08-18 46 | 47 | ### Added 48 | 49 | - Minimum required Node version as `engines` in `package.json` 50 | - Every `.js` file in `src/` is considered an entrypoint 51 | - Multiple entrypoints result in multiple scripts 52 | - A single entrypoint is renamed to `name` from `package.json` 53 | 54 | ### Changed 55 | 56 | - Minification does not rewrite syntax to avoid Extendscript errors 57 | 58 | ## [0.0.5] - 2022-08-12 59 | 60 | ### Added 61 | 62 | - Exposes `PRODUCT_DISPLAY_NAME` environment variable 63 | - Copies static files from `/static` 64 | 65 | ### Changed 66 | 67 | - Renamed entrypoint to `main.js` instead of `app.js` 68 | 69 | ### Fixed 70 | 71 | - Improves debug configuration 72 | 73 | ## [0.0.1] - 2022-08-11 74 | 75 | Initial "release" 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Remco Janssen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extender 2 | 3 | A modern starter for writing Adobe Extendscript 4 | 5 | _(yes, another one)_ 6 | 7 | ## Why? 8 | 9 | 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. 10 | 11 | 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)._ 12 | 13 | ## Features 14 | 15 | - Modern Javascript with [ponyfills](https://github.com/sindresorhus/ponyfill#how-are-ponyfills-better-than-polyfills) (see [babel-preset-extendscript](https://github.com/fusepilot/babel-preset-extendscript)) 16 | - Ecmascript modules for importing and exporting 17 | - Fast bundling with [esbuild](https://github.com/evanw/esbuild) _(TODO: insert obligatory lightning bolt emoji)_ 18 | - Rebundles on file changes 19 | - Minifies the release version 20 | - Converts to binary with [extendscript-debugger](https://marketplace.visualstudio.com/items?itemName=Adobe.extendscript-debug) 21 | - Wraps bundle in an [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) to avoid global variables 22 | - Exposes environment variables to Javascript files 23 | - Includes JSON automatically as a ponyfill 24 | - Copies static files from `/static` (with [esbuild-copy-static-files](https://github.com/nickjj/esbuild-copy-static-files)) 25 | - Imports `?text` suffixed paths as strings 26 | - Imports icons as binary strings 27 | 28 | ## Try the example 29 | 30 | 1. Duplicate `.env.example` and remove the `.example` extension 31 | 1. Run `npm install && npm start` in your terminal 32 | 1. Run `/build/extender.jsx` in your Adobe app of choice 33 | 34 | ## Development 35 | 36 | ``` 37 | npm install && npm start 38 | ``` 39 | 40 | This will start watching your source files and builds into the `build` folder. 41 | 42 | ## Release 43 | 44 | ``` 45 | npm run release 46 | ``` 47 | 48 | This will bundle, minify and jsxbin your source files into the `dist` folder. 49 | 50 | ## Environment Variables 51 | 52 | All variables are replaced by their values upon bundling. 53 | 54 | By default the bundler exposes: 55 | 56 | - `DEVMODE` when `NODE_ENV` is `development` or not, 57 | - `PRODUCT_NAME` which is `name` from `package.json`, 58 | - `PRODUCT_DISPLAY_NAME` which is `displayName` from `package.json` and 59 | - `PRODUCT_VERSION` which is `version` from `package.json` 60 | 61 | If you have a `.env` file it will automatically expose the variables by their name to all Javascript files. 62 | 63 | ## Entrypoints 64 | 65 | Every `.js` file in the root of the `source/` 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`. 66 | 67 | ## Debugging 68 | 69 | 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` 70 | 71 | You can't use breakpoints in your source files, because the [Extendscript Debugger](https://marketplace.visualstudio.com/items?itemName=Adobe.extendscript-debug) 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`). 72 | 73 | ## Import JavaScript as String 74 | 75 | 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. 76 | 77 | See [src/main.js](./src/main.js#L6) 78 | 79 | ## Import Icons as Binary String 80 | 81 | Using icons in [scriptUI](https://extendscript.docsforadobe.dev/user-interface-tools/) 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()`](https://extendscript.docsforadobe.dev/file-system-access/file-object.html#decode). 82 | 83 | See [src/main.js](./src/main.js#L17) 84 | 85 | ## Import Node Modules 86 | 87 | 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](https://github.com/fusepilot/babel-preset-extendscript#features). 88 | 89 | ## Static Files 90 | 91 | 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. 92 | 93 | ## Minification 94 | 95 | Only whitespaces are removed and variable names are shortened. The syntax remains intact to avoid Extendscript errors. 96 | 97 | ## Types for Adobe 98 | 99 | [Types for Adobe](https://github.com/aenhancers/Types-for-Adobe) is part of the package, so you can simply use [triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) to get the type definitions for your target app. Or create a [`tsconfig.json`](https://github.com/hyperbrew/bolt-cep/blob/master/src/jsx/tsconfig.json) to define which types to use in the whole project. 100 | 101 | See [src/main.js](./src/main.js#L1-L2) 102 | 103 | ## Typescript 104 | 105 | You should even be able to [make it work with Typescript](https://esbuild.github.io/content-types/#typescript) if that's your thing. 106 | 107 | ## By the way 108 | 109 | You can still write regular old Extendscript without the modern syntax. 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extender", 3 | "displayName": "Extender", 4 | "version": "0.0.10", 5 | "type": "module", 6 | "private": true, 7 | "engines": { 8 | "node": ">=16.7.0" 9 | }, 10 | "scripts": { 11 | "start": "cross-env NODE_ENV=development npm-run-all build", 12 | "release": "cross-env NODE_ENV=production run-s build jsxbin", 13 | "jsxbin": "node ./scripts/jsxbin.js", 14 | "build": "node ./scripts/build.js" 15 | }, 16 | "devDependencies": { 17 | "babel-preset-extendscript": "^1.0.2", 18 | "cross-env": "^7.0.3", 19 | "dotenv": "^16.0.1", 20 | "esbuild": "^0.14.10", 21 | "esbuild-copy-static-files": "^0.1.0", 22 | "esbuild-plugin-babel": "^0.2.3", 23 | "fs-extra": "^10.0.0", 24 | "glob": "^8.0.3", 25 | "just-merge": "^3.1.1", 26 | "npm-run-all": "^4.1.5", 27 | "prettier": "^2.5.1", 28 | "readdirp": "^3.6.0", 29 | "types-for-adobe": "^7.0.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/binary.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | 3 | export default function binaryString() { 4 | return { 5 | name: 'binary', 6 | setup(build) { 7 | build.onLoad({ filter: /\.png|jpg$/ }, async (args) => { 8 | const filePath = args.path 9 | const data = await fs.readFile(filePath) 10 | const bin = data.toString('binary') 11 | return { 12 | contents: encodeURIComponent(bin), 13 | loader: 'text' 14 | } 15 | }) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import copyStaticFiles from 'esbuild-copy-static-files' 2 | import babel from 'esbuild-plugin-babel' 3 | import binaryString from './binary.js' 4 | import textLoader from './text.js' 5 | import { build } from 'esbuild' 6 | import { join } from 'path' 7 | import fs from 'fs-extra' 8 | import glob from 'glob' 9 | import 'dotenv/config' 10 | 11 | const entryPoints = glob.sync('src/*.js') 12 | const devmode = process.env.NODE_ENV === 'development' 13 | const outdir = devmode ? 'build' : 'dist' 14 | const pkg = await fs.readJson('./package.json') 15 | const out = entryPoints.length === 1 ? { outfile: join(outdir, `${pkg.name}.jsx`) } : { outdir } 16 | const define = { 17 | 'DEVMODE': devmode, 18 | 'PRODUCT_NAME': JSON.stringify(pkg.name), 19 | 'PRODUCT_DISPLAY_NAME': JSON.stringify(pkg.displayName), 20 | 'PRODUCT_VERSION': JSON.stringify(pkg.version), 21 | } 22 | 23 | for (const key in process.env) { 24 | const invalid = key.includes('(x86)') 25 | if (!invalid) { 26 | define[key] = JSON.stringify(process.env[key]) 27 | } 28 | } 29 | 30 | build({ 31 | ...out, 32 | define, 33 | entryPoints, 34 | logLevel: 'info', 35 | bundle: true, 36 | sourcemap: devmode, 37 | target: ['es5'], 38 | minifyWhitespace: !devmode, 39 | minifyIdentifiers: !devmode, 40 | outExtension: { '.js': '.jsx' }, 41 | plugins: [ 42 | copyStaticFiles({ dest: outdir }), 43 | binaryString(), 44 | textLoader(), 45 | babel({ 46 | config: { 47 | presets: [ 48 | ['extendscript', { modules: false }] 49 | ] 50 | } 51 | }), 52 | ], 53 | watch: devmode && { 54 | onRebuild(error) { 55 | if (error) console.error(error) 56 | }, 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /scripts/jsxbin.js: -------------------------------------------------------------------------------- 1 | import { renameSync, existsSync } from 'fs' 2 | import { readdir } from 'fs/promises' 3 | import { fork } from 'child_process' 4 | import readdirp from 'readdirp' 5 | import { homedir } from 'os' 6 | import path from 'path' 7 | 8 | const devmode = process.env.NODE_ENV === 'development' 9 | const outdir = devmode ? 'build' : 'dist' 10 | 11 | const curDir = path.resolve(outdir) 12 | const foundScripts = await readdirp.promise(curDir, { fileFilter: '*.jsx' }) 13 | const scripts = foundScripts.map((f) => f.fullPath) 14 | const exportJSXBin = await getExtensionPath() 15 | 16 | scripts.forEach((script) => { 17 | fork(exportJSXBin, ['-f', '-n', script]) 18 | .on('close', () => renameSync(`${script}bin`, script)) 19 | }) 20 | 21 | async function getExtensionPath() { 22 | const extensionsPath = path.join(homedir(), '.vscode', 'extensions') 23 | if (!existsSync(extensionsPath)) { 24 | throw new Error(`Missing VSCode extensions folder at ${extensionsPath}`) 25 | } 26 | const extensions = await readdir(extensionsPath) 27 | const extensionName = 'adobe.extendscript-debug' 28 | const extendscriptFolder = extensions.find((f) => f.includes(extensionName)) 29 | if (!extendscriptFolder) { 30 | throw new Error(`Missing VSCode extension ${path.join(extensionsPath, extensionName)}`) 31 | } 32 | const jsxBinPath = path.join(extensionsPath, extendscriptFolder, 'public-scripts', 'exportToJSXBin.js') 33 | if (!existsSync(jsxBinPath)) { 34 | throw new Error(`Expected script at ${jsxBinPath}`) 35 | } 36 | return jsxBinPath 37 | } 38 | -------------------------------------------------------------------------------- /scripts/text.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises' 2 | import { join, isAbsolute } from 'path' 3 | 4 | // SOURCE: https://github.com/hannoeru/esbuild-plugin-raw 5 | export default function textLoader() { 6 | const namespace = 'text-loader' 7 | const filter = /\?text$/ 8 | return { 9 | name: 'textloader', 10 | setup(build) { 11 | build.onResolve({ filter }, (args) => { 12 | return { 13 | namespace, 14 | path: args.path, 15 | pluginData: { 16 | isAbsolute: isAbsolute(args.path), 17 | resolveDir: args.resolveDir, 18 | }, 19 | } 20 | }) 21 | build.onLoad({ filter, namespace }, async ({ path, pluginData }) => { 22 | const { resolveDir, isAbsolute } = pluginData 23 | const fullPath = isAbsolute ? path : join(resolveDir, path) 24 | const withoutSuffix = fullPath.replace(filter, '') 25 | return { 26 | contents: await readFile(withoutSuffix), 27 | loader: 'text', 28 | } 29 | }) 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/icons/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klustre/extender/cae1cd851bcd01a265c64bef0e5c9974e9d32792/src/icons/icon.jpg -------------------------------------------------------------------------------- /src/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Klustre/extender/cae1cd851bcd01a265c64bef0e5c9974e9d32792/src/icons/icon.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { notify } from './modules/utils.js' 5 | import merge from 'just-merge' 6 | import expression from './modules/expression.js?text' 7 | import icon from './icons/icon.png' 8 | 9 | const obj = { a: 3, b: 5 } 10 | const merged = merge(obj, { a: 4, c: 8 }) 11 | notify(`My merged object in JSON:\n${JSON.stringify(merged, '', 2)}`) 12 | 13 | const greetings = [expression, IM_IN_ENV] 14 | greetings.forEach(notify) 15 | 16 | const dialog = new Window('dialog') 17 | dialog.add('iconbutton', undefined, File.decode(icon)) 18 | dialog.show() 19 | -------------------------------------------------------------------------------- /src/modules/expression.js: -------------------------------------------------------------------------------- 1 | // NOTE: I'm an expression for After Effects 2 | const x = 1920; 3 | const y = 1080; 4 | [value[0] + x, value[1] + y] 5 | -------------------------------------------------------------------------------- /src/modules/utils.js: -------------------------------------------------------------------------------- 1 | export function notify(msg) { 2 | alert(`${PRODUCT_DISPLAY_NAME} ${PRODUCT_VERSION}\n${msg}`) 3 | } 4 | -------------------------------------------------------------------------------- /static/README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | README 8 | 9 | 10 |

11 | Redirecting to https://supa.supply/ 12 |

13 | 16 | 17 | 18 | --------------------------------------------------------------------------------