jsxk
Version:
Provides a two-way communication interface between NodeJS and Adobe ExtendScript (JSX) files
248 lines (176 loc) • 5.82 kB
JavaScript
const fs = require('fs');
const path = require('path');
const open = require('open');
const JSX_JSONPOLYFILL_PATH = path.join(__dirname,'jsx','json2.js');
const THROW_ERROR_IF_VAR_NOT_FOUND = true;
const jsxk = {}
jsxk.defaultOptions = {targetProcess:null, timeout:60};
jsxk.options = {};
jsxk.exec = function(jsxFilePath, vars, callback){
let options = Object.assign({}, jsxk.defaultOptions, jsxk.options);
if (!fs.existsSync(jsxFilePath) || fs.statSync(jsxFilePath).isDirectory()){
throw new Error('Invalid jsxFilePath `'+jsxFilePath+'`');
}
let jsxSrc;
try {
jsxSrc = fs.readFileSync(jsxFilePath, 'utf8');
} catch (err) {
throw err;
}
let tmpJsxFilePath;
let c = 0;
while (true){
const pathParts = jsxFilePath.split('.');
pathParts.splice(pathParts.length-1, 0, 'working' + pad(c,3));
tmpJsxFilePath = pathParts.join('.');
c++;
if (!fs.existsSync(tmpJsxFilePath)){
break;
}
}
// Inject variables into temp file
let tmpJsxSrc = jsxk.inject(jsxSrc, vars);
try {
fs.writeFileSync(tmpJsxFilePath, tmpJsxSrc, 'utf8');
} catch (err) {
throw err;
}
if (!fs.existsSync(tmpJsxFilePath)){
throw new Error('Unable to create `'+tmpJsxFilePath+'`');
}
let _options = {};
if (options.targetProcess){
_options.app = options.targetProcess; // 'Adobe Photoshop 2020';
}
const jsxExec = open(tmpJsxFilePath, _options);
const checkForCompletion = new Promise(function(resolve, reject){
const CHECK_FILE_INTERVAL_MS = 50;
const TIMEOUT_MS = options.timeout*1000;
let totalTime = 0;
let checkInterval = setInterval(function(){
totalTime += CHECK_FILE_INTERVAL_MS;
if (fs.existsSync(tmpJsxFilePath)){
let fileContents;
try {
fileContents = fs.readFileSync(tmpJsxFilePath, 'utf8');
} catch (err) {
// Failed to read contents
return reject(err);
}
if (fileContents.split('__jsxkResultIN').length == 2){
clearInterval(checkInterval);
let data;
try {
let jsonRaw = fileContents.split('__jsxkResultIN')[1].split('__jsxkResultOUT')[0];
data = JSON.parse(jsonRaw);
} catch (err) {
// Failed to parse
return reject(err);
}
return resolve(data);
} else {
if (totalTime > TIMEOUT_MS){
clearInterval(checkInterval);
return reject(new Error('JSX Script time out reached ('+String(Math.round(TIMEOUT_MS*10)/10)+'secs)'));
}
}
} else {
return reject(new Error('Temp file not found.'))
}
}, CHECK_FILE_INTERVAL_MS);
});
const deleteTempFile = function(){
return new Promise(function(resolve, reject){
try {
fs.unlinkSync(tmpJsxFilePath);
} catch (err) {
return reject(err);
}
if (fs.existsSync(tmpJsxFilePath)){
return reject(new Error('Failed to delete temp file'));
}
return resolve();
});
}
let chain = Promise.all([jsxExec,checkForCompletion]).then(function(data){
return deleteTempFile().then(function(){
return data[1];
})
}).catch(function(err){
const throwError = function(delFileErr){
return new Promise(function(resolve, reject){
return reject(err ? err : (delFileErr ? delFileErr : new Error('Unspecified error')));
});
}
// Attempt to delete the temp file, throw the error always
return deleteTempFile().then(throwError, throwError);
});
if (callback){
chain.then(
function(data) {
callback(null, data);
},
function(err) {
callback(err);
}
)
} else {
return chain;
}
}
jsxk.inject = function(jsxSrc, vars){
for (let varName in vars){
const regex = new RegExp('^[^\S\r\n]*((?:var|const|let) '+escapeRegExp(varName)+'[^\S\r\n]*=[^\S\r\n]*?)(.*)$', 'mi'); // Find first variable declaration, case insensitive
let m;
if ((m = regex.exec(jsxSrc)) !== null) {
jsxSrc = jsxSrc.substr(0, m.index) + m[1] + JSON.stringify(vars[varName]) + ';' + jsxSrc.substr(m.index+m[0].length)
} else if (THROW_ERROR_IF_VAR_NOT_FOUND){
throw new Error('Variable `'+varName+'` not found in JSX src.')
}
}
// Add JSON
if (!fs.existsSync(JSX_JSONPOLYFILL_PATH)){
throw new Error('Invalid JSX_JSONPOLYFILL_PATH `'+JSX_JSONPOLYFILL_PATH+'`');
}
let jsxJSONPolyfillSrc;
try {
jsxJSONPolyfillSrc = fs.readFileSync(JSX_JSONPOLYFILL_PATH, 'utf8');
} catch (err) {
throw err;
}
// Add completion
jsxSrc += `
// jsxk.js - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function jsxOnComplete(data){
`+jsxJSONPolyfillSrc+`
data = typeof data !== 'undefined' ? data : {};
// Write result as JSON string appended to bottom of this script.
var f = File($.fileName);
f.encoding = "UTF-8";
f.lineFeed = "Unix";
f.open("a");
f.writeln('/'+'*__'+'jsxkResultIN' + JSON.stringify(data) + '__'+'jsxkResultOUT*'+'/');
f.close();
}
`;
// Add callback if it hasn't been made yet
if (jsxSrc.split('jsxOnComplete').length == 2){
jsxSrc += '\jsxOnComplete();\n';
}
return jsxSrc;
}
module.exports = jsxk;
// Utils
// -----
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function pad(num, charNum){
var str = String(num);
var len = charNum-str.length;
for (var i = 0; i < len; i++){
str = '0' + str;
}
return str;
}