diff --git a/README.md b/README.md index e36756a..2f79899 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,14 @@ npm install rollup-plugin-ae-jsx --save-dev Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files), import the plugin, and add it to the `plugins` array: ```js -import afterEffectJsx from "./rollup-plugin-ae-jsx"; -import pkg from "./package.json"; +import afterEffectJsx from './rollup-plugin-ae-jsx'; +import pkg from './package.json'; export default { - input: "src/index.ts", + input: 'src/index.ts', output: { - file: "dist/index.jsx", - format: "es", + file: 'dist/index.jsx', + format: 'es', }, external: Object.keys(pkg.dependencies), plugins: [afterEffectJsx()], @@ -44,13 +44,86 @@ export default { Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api). -## Proccess +## Options + +### `wrap` + +Type: `boolean` \ +Default: `false` + +Wraps compiled code in a `get()` function. See [Wrapping](#wrapping) for more detail. + +## Process 1. Creating a list of the exported functions and variables from the index file 2. Removing non-compatible statements: `['ExpressionStatement', 'DebuggerStatement', 'ImportDeclaration', 'ExportNamedDeclaration'];` 3. Converting function and variable declarations into `.jsx` compliant syntax 4. Wrapping in braces (`{}`) +## Wrapping + +Compiling code that references top level functions or variables will error when run in After Effects, since each exported property is isolated from the surrounding code. + +For example the following source code: + +```js +function add(a, b) { + return a + b; +} + +function getFour() { + return add(2, 2); +} + +export { add, getFour }; +``` + +Will compile to the following `.jsx` file: + +```js +{ + add(a, b) { + return a + b; + }, + getFour() { + return add(2, 2); // error, add is not defined + } +} +``` + +Which will error, since `add()` is not defined within the scope of `getFour()`. + +This can be solved by wrapping all of your code in a parent function, which `rollup-plugin-jsx` will do for you if `wrap` is set to true. + +```js +// rollup.config.js +plugins: [afterEffectJsx({ wrap: true })], +``` + +The compiled `.jsx` would then be: + +```js +{ + get() { + function add(a, b) { + return a + b; + } + + function getFour() { + return add(2, 2); + } + + return { add, getFour } + } +} +``` + +You then would need to call `.get()` in your expressions: + +```js +const { getFour, add } = footage('index.jsx').sourceData.get(); +``` + ## Meta [CONTRIBUTING](/.github/CONTRIBUTING.md) diff --git a/package-lock.json b/package-lock.json index 441f157..2e4c606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,42 +1,73 @@ { "name": "rollup-plugin-ae-jsx", "version": "2.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "estree-walker": { + "packages": { + "": { + "name": "rollup-plugin-ae-jsx", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "magic-string": "^0.25.9" + }, + "devDependencies": { + "rollup": "^2.23.0" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/estree-walker": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz", "integrity": "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==" }, - "fsevents": { + "node_modules/fsevents": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", "dev": true, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "requires": { - "sourcemap-codec": "^1.4.4" + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" } }, - "rollup": { + "node_modules/rollup": { "version": "2.23.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.23.1.tgz", "integrity": "sha512-Heyl885+lyN/giQwxA8AYT2GY3U+gOlTqVLrMQYno8Z1X9lAOpfXPiKiZCyPc25e9BLJM3Zlh957dpTlO4pa8A==", "dev": true, - "requires": { + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { "fsevents": "~2.1.2" } }, - "sourcemap-codec": { + "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" } } } diff --git a/package.json b/package.json index a6dcfc7..71e4027 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,10 @@ "homepage": "https://github.com/motiondeveloper/rollup-plugin-ae-jsx#readme", "dependencies": { "estree-walker": "^2.0.1", - "magic-string": "^0.25.7" + "magic-string": "^0.25.9" + }, + "prettier": { + "useTabs": false, + "singleQuote": true } } diff --git a/src/index.js b/src/index.js index 9652540..d0f9c9f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,22 +1,26 @@ -import { walk } from "estree-walker"; -import MagicString from "magic-string"; +import { walk } from 'estree-walker'; +import MagicString from 'magic-string'; const whitespace = /\s/; + // these will be removed const disallowedNodeTypes = [ - "ExpressionStatement", - "DebuggerStatement", - "ImportDeclaration", - "ExportNamedDeclaration", + 'ExpressionStatement', + 'DebuggerStatement', + 'ImportDeclaration', + 'ExportNamedDeclaration', ]; -export default function afterEffectsJsx(options = {}) { +// these will also be removed +const expressionGlobals = ['thisComp', 'thisLayer', 'thisProperty']; + +export default function afterEffectsJsx(options = { wrap: false }) { const exports = []; + const wrap = options.wrap; return { - name: "after-effects-jsx", // this name will show up in warnings and errors + name: 'after-effects-jsx', // this name will show up in warnings and errors generateBundle(options = {}, bundle, isWrite) { - // format each file - // to be ae-jsx + // format each file to be ae-jsx for (const file in bundle) { // Get the string code of the file let code = bundle[file].code; @@ -29,7 +33,7 @@ export default function afterEffectsJsx(options = {}) { throw err; } // create magic string to perform operations on - const magicString = new MagicString(code); + let magicString = new MagicString(code); // removes characters from the magicString function remove(start, end) { @@ -39,7 +43,7 @@ export default function afterEffectsJsx(options = {}) { function isBlock(node) { return ( - node && (node.type === "BlockStatement" || node.type === "Program") + node && (node.type === 'BlockStatement' || node.type === 'Program') ); } @@ -50,7 +54,7 @@ export default function afterEffectsJsx(options = {}) { if (isBlock(parent)) { remove(node.start, node.end); } else { - magicString.overwrite(node.start, node.end, "(void 0);"); + magicString.overwrite(node.start, node.end, '(void 0);'); } } @@ -58,7 +62,7 @@ export default function afterEffectsJsx(options = {}) { // that are exports.[exportName] = [exportName]; walk(ast, { enter(node, parent) { - Object.defineProperty(node, "parent", { + Object.defineProperty(node, 'parent', { value: parent, enumerable: false, configurable: true, @@ -66,7 +70,7 @@ export default function afterEffectsJsx(options = {}) { if ( // it's an export expression statement - node.type === "ExportNamedDeclaration" + node.type === 'ExportNamedDeclaration' ) { exports.push( ...node.specifiers.map((exportNode) => exportNode.local.name) @@ -75,72 +79,115 @@ export default function afterEffectsJsx(options = {}) { }, }); - // Remove non exported nodes and convert - // to object property style compatible syntax - walk(ast, { - enter(node, parent) { - Object.defineProperty(node, "parent", { - value: parent, - enumerable: false, - configurable: true, - }); + if (wrap) { + // Remove expression globals and unsupported code + // then wrap in get() method. Less work is needed in this case + // everything is wrapped in a single function + walk(ast, { + enter(node, parent) { + Object.defineProperty(node, 'parent', { + value: parent, + enumerable: false, + configurable: true, + }); - if (node.type === "FunctionDeclaration") { - // Deal with functions - const functionName = node.id.name; - if (!exports.includes(functionName)) { - // Remove non-exported functions - remove(node.start, node.end); - } else { - // remove the function keyword - magicString.remove(node.start, node.id.start); - // add a trailing comma - magicString.appendLeft(node.end, ","); + if (node.type === 'VariableDeclaration') { + const variableName = node.declarations.map( + (declaration) => declaration.id.name + )[0]; + + if (expressionGlobals.includes(variableName)) { + // Remove temporary expression global declarations + remove(node.start, node.end); + } + // don't process child nodes + this.skip(); + } else if (disallowedNodeTypes.includes(node.type)) { + // Remove every top level node that isn't + // a function or variable, as they're not allowed + removeStatement(node); + this.skip(); } - // don't process child nodes - this.skip(); - } else if (node.type === "VariableDeclaration") { - // deal with variables - const variableName = node.declarations.map( - (declaration) => declaration.id.name - )[0]; - if (!exports.includes(variableName)) { - // Remove variables that aren't exported - remove(node.start, node.end); - } else { - const valueStart = node.declarations[0].init.start; - const variableName = node.declarations[0].id.name; - // remove anything before the variable name - // e.g. const, var, let - magicString.overwrite( - node.start, - valueStart - 1, - `${variableName}:` - ); - const endsInSemiColon = - magicString.slice(node.end - 1, node.end) === ";"; - if (endsInSemiColon) { - // replace ; with , - magicString.overwrite(node.end - 1, node.end, ","); + }, + }); + + magicString + // add return statements for exports + .append(`\nreturn { ${exports.join(', ')} }`) + // indent everything before wrapping + .indent() + // wrap entire code in get() method + .prepend('get() {\n') + .append('\n}'); + } else { + // Remove non exported nodes and convert + // to object property style compatible syntax + walk(ast, { + enter(node, parent) { + Object.defineProperty(node, 'parent', { + value: parent, + enumerable: false, + configurable: true, + }); + + if (node.type === 'FunctionDeclaration') { + // Deal with functions + const functionName = node.id.name; + if (!exports.includes(functionName)) { + // Remove non-exported functions + remove(node.start, node.end); } else { - // or add trailing comma - magicString.appendLeft(node.end, ","); + // remove the function keyword + magicString.remove(node.start, node.id.start); + // add a trailing comma + magicString.appendLeft(node.end, ','); } + // don't process child nodes + this.skip(); + } else if (node.type === 'VariableDeclaration') { + // deal with variables + const variableName = node.declarations.map( + (declaration) => declaration.id.name + )[0]; + if (!exports.includes(variableName)) { + // Remove variables that aren't exported + remove(node.start, node.end); + } else { + const valueStart = node.declarations[0].init.start; + const variableName = node.declarations[0].id.name; + // remove anything before the variable name + // e.g. const, var, let + magicString.overwrite( + node.start, + valueStart - 1, + `${variableName}:` + ); + const endsInSemiColon = + magicString.slice(node.end - 1, node.end) === ';'; + if (endsInSemiColon) { + // replace ; with , + magicString.overwrite(node.end - 1, node.end, ','); + } else { + // or add trailing comma + magicString.appendLeft(node.end, ','); + } + } + // don't process child nodes + this.skip(); + } else if (disallowedNodeTypes.includes(node.type)) { + // Remove every top level node that isn't + // a function or variable, as they're not allowed + removeStatement(node); + this.skip(); } - // don't process child nodes - this.skip(); - } else if (disallowedNodeTypes.includes(node.type)) { - // Remove every top level node that isn't - // a function or variable, as they're not allowed - removeStatement(node); - this.skip(); - } - }, - }); + }, + }); + } + // Log exports to the terminal console.log(`Exported JSX:`, exports); // Sanitize output and wrap in braces - magicString.trim().indent().prepend("{\n").append("\n}"); + magicString.trim().indent().prepend('{\n').append('\n}').trimStart(); // Replace the files code with modified bundle[file].code = magicString.toString(); }