namespace OS { // import the CodePad application module const App = OS.application.CodePad; declare var CoffeeScript: any; declare var JSZip: any; /** * * * @class ExtensionMaker * @extends {App.BaseExtension} */ class ExtensionMaker extends App.BaseExtension { constructor(app: application.CodePad) { super(app); } // public functions /** * * * @memberof ExtensionMaker */ create(): void { this.logger().clear(); this.app .openDialog("FileDialog", { title: "__(New CodePad extension at)", file: { basename: __("ExtensionName") }, mimes: ["dir"], }) .then((d) => { return this.mktpl(d.file.path, d.name); }); } /** * * * @memberof ExtensionMaker */ buildnrun(): void { this.logger().clear(); this.metadata("extension.json") .then(async (meta) => { try { await this.build(meta); try { return this.run(meta); } catch (e) { return this.logger().error(__("Unable to run extension: {0}", e.stack)); } } catch (e_1) { return this.logger().error(__("Unable to build extension: {0}", e_1.stack)); } }) .catch((e) => this.logger().error(__("Unable to read meta-data:{0}", e.stack))); } /** * * * @memberof ExtensionMaker */ release(): void { this.logger().clear(); this.metadata("extension.json") .then(async (meta) => { try { await this.build(meta); try { return this.mkar( `${meta.root}/build/debug`, `${meta.root}/build/release/${meta.meta.name}.zip` ); } catch (e) { return this.logger().error( __("Unable to create archive: {0}", e.stack )); } } catch (e_1) { return this.logger().error(__("Unable to build extension: {0}", e_1.stack)); } }) .catch((e) => this.logger().error(__("Unable to read meta-data: {0}", e.stack))); } /** * * * @memberof ExtensionMaker */ installFromURL(): void { this.logger().clear(); this.app .openDialog("PromptDialog", { title: __("Enter URI"), label: __("Please enter extension URI:") }) .then(async (v) => { if(!v) return; try { await this.installZip(v); this.logger().info(__("Extension installed")); return this.app.loadExtensionMetaData(); } catch (e) { return this.app.error(__("Unable to install extension: {0}", v)); } }); } /** * * * @memberof ExtensionMaker */ install(): void { this.logger().clear(); this.app .openDialog("FileDialog", { title: "__(Select extension archive)", mimes: [".*/zip"], }) .then(async (d) => { try { await this.installZip(d.file.path); this.logger().info(__("Extension installed")); return this.app.loadExtensionMetaData(); } catch (e) { return this.logger().error(__("Unable to install extension: {0}", e.stack)); } }); } get ext_dir(): string { return "home://.codepad"; } /** * * * @private * @param {string} path * @param {string} name * @memberof ExtensionMaker */ private mktpl(path: string, name: string): void { const rpath = `${path}/${name}`; const dirs = [ rpath, `${rpath}/build`, `${rpath}/build/release`, `${rpath}/build/debug`, ]; const files = [ ["templates/ext-main.tpl", `${rpath}/${name}.coffee`], ["templates/ext-extension.tpl", `${rpath}/extension.json`], ]; this.mkdirAll(dirs) .then(async () => { try { await this.mkfileAll(files, path, name); this.app.currdir = rpath.asFileHandle(); this.app.toggleSideBar(); return this.app.eum.active.openFile( `${rpath}/${name}.coffee`.asFileHandle() as application.CodePadFileHandle ); } catch (e) { return this.logger().error( __("Unable to create extension template: {0}", e.stack) ); } }) .catch((e) => this.logger().error(__("Unable to create extension directories: {0}", e.stack)) ); } /** * * * @private * @param {string[]} list * @returns {Promise} * @memberof ExtensionMaker */ private verify(list: string[]): Promise { return new Promise((resolve, reject) => { if (list.length === 0) { return resolve(); } const file = list.splice(0, 1)[0].asFileHandle(); this.logger().info(__("Verifying: {0}", file.path)); return file .read() .then((data) => { try { CoffeeScript.nodes(data); return this.verify(list) .then(() => resolve()) .catch((e) => reject(__e(e))); } catch (ex) { return reject(__e(ex)); } }) .catch((e) => reject(__e(e))); }); } /** * * * @private * @param {GenericObject} meta * @returns {Promise} * @memberof ExtensionMaker */ private compile(meta: GenericObject): Promise { return new Promise(async (resolve, reject) => { try { await this.import([`${this.basedir()}/libs/coffeescript.js`]); const list = meta.coffees.map( (v) => `${meta.root}/${v}` ); try { await this.verify(list.map((x: string) => x)); try { const code = await this.cat(list, ""); const jsrc = CoffeeScript.compile(code); this.logger().info(__("Compiled successful")); return resolve(jsrc); } catch (e) { return reject(__e(e)); } } catch (e_1) { return reject(__e(e_1)); } } catch (e_2) { return reject(__e(e_2)); } }); } /** * * * @private * @param {GenericObject} meta * @returns {Promise} * @memberof ExtensionMaker */ private build(meta: GenericObject): Promise { return new Promise(async (resolve, reject) => { try { const src = await this.compile(meta); let v: string; try { const jsrc = await this.cat( (() => { const result = []; for (v of meta.javascripts) { result.push(`${meta.root}/${v}`); } return result; })(), src ); await new Promise((r, e) => `${meta.root}/build/debug/${meta.meta.name}.js` .asFileHandle() .setCache(jsrc) .write("text/plain") .then((d) => r()) .catch((ex) => e(__e(ex))) ); await new Promise((r, e) => `${meta.root}/build/debug/extension.json` .asFileHandle() .setCache(meta.meta) .write("object") .then((data) => r(data)) .catch((ex_1) => e(__e(ex_1))) ); await this.copy( (() => { const result1 = []; for (v of meta.copies) { result1.push(`${meta.root}/${v}`); } return result1; })(), `${meta.root}/build/debug` ); return resolve(); } catch (e) { return reject(__e(e)); } } catch (e_1) { return reject(__e(e_1)); } }); } /** * * * @private * @param {GenericObject} meta * @returns {Promise} * @memberof ExtensionMaker */ private run(meta: GenericObject): Promise { return new Promise(async (resolve, reject) => { const path = `${meta.root}/build/debug/${meta.meta.name}.js`; if (API.shared[path]) { delete API.shared[path]; } try { await API.requires(path); let v: GenericObject; if (this.app.extensions[meta.meta.name]) { this.app.extensions[meta.meta.name].text = meta.meta.text; this.app.extensions[meta.meta.name].nodes = []; if(this.app.extensions[meta.meta.name].ext && this.app.extensions[meta.meta.name].ext.cleanup) { this.app.extensions[meta.meta.name].ext.cleanup(); } this.app.extensions[meta.meta.name].ext = new App.extensions[meta.meta.name](this.app); for (v of meta.meta.actions) { this.app.extensions[meta.meta.name].addAction(v); } } else { this.app.extensions[meta.meta.name] = new App.CMDMenu( meta.meta.text ); this.app.extensions[meta.meta.name].name = meta.meta.name; for (v of meta.meta.actions) { this.app.extensions[meta.meta.name].addAction(v); } this.app.spotlight.addAction( this.app.extensions[meta.meta.name] ); this.app.extensions[meta.meta.name].onchildselect( (e: GUI.TagEventType) => { return this.app.loadAndRunExtensionAction( e.data.item.data as any ); } ); } this.app.spotlight.run(this.app); return resolve(); } catch (e) { return reject(__e(e)); } }); } /** * * * @private * @param {string[]} files * @param {*} zip * @returns {Promise} * @memberof ExtensionMaker */ private installExtension(files: string[], zip: any): Promise { return new Promise((resolve, reject) => { const idx = files.indexOf("extension.json"); if (idx < 0) { reject(API.throwe(__("No meta-data found"))); } const metafile = files.splice(idx, 1)[0]; // read the meta file return zip .file(metafile) .async("uint8array") .then((d: Uint8Array) => { const meta = JSON.parse( new TextDecoder("utf-8").decode(d) ); return this.installFiles(files, zip, meta) .then(() => resolve()) .catch((e) => reject(__e(e))); }) .catch((e: Error) => reject(__e(e))); }); } /** * * * @private * @param {string[]} files * @param {*} zip * @param {GenericObject} meta * @returns {Promise} * @memberof ExtensionMaker */ private installFiles( files: string[], zip: any, meta: GenericObject ): Promise { if (files.length === 0) { return this.installMeta(meta); } return new Promise((resolve, reject) => { const file = files.splice(0, 1)[0]; const path = `${this.ext_dir}/${file}`; return zip .file(file) .async("uint8array") .then((d: Uint8Array) => { return path .asFileHandle() .setCache(new Blob([d], { type: "octet/stream" })) .write("text/plain") .then((r) => { if (r.error) { return reject(r.error); } return this.installFiles(files, zip, meta) .then(() => resolve()) .catch((e) => reject(__e(e))); }) .catch((e) => reject(__e(e))); }) .catch((e: Error) => reject(__e(e))); }); } /** * * * @private * @param {GenericObject} meta * @returns {Promise} * @memberof ExtensionMaker */ private installMeta(meta: GenericObject): Promise { return new Promise(async (resolve, reject) => { const file = `${this.ext_dir}/extensions.json`.asFileHandle(); try { const data = await file.read("json"); const names = []; for (let v of data) { names.push(v.name); } const idx = names.indexOf(meta.name); if (idx >= 0) { data.splice(idx, 1); } data.push(meta); try { await file.setCache(data).write("object"); return resolve(); } catch (e) { return reject(__e(e)); } } catch (e_1) { // try to create new file try { await file.setCache([meta]).write("object"); return resolve(); } catch (e_2) { return reject(__e(e_2)); } } }); } /** * * * @private * @param {string} path * @returns {Promise} * @memberof ExtensionMaker */ private installZip(path: string): Promise { return new Promise((resolve, reject) => { this.import(["os://scripts/jszip.min.js"]) .then(() => { path.asFileHandle() .read("binary") .then((data) => { JSZip.loadAsync(data) .then((zip: any) => { const pth = this.ext_dir; const dir = [this.ext_dir]; const files = []; for (let name in zip.files) { const file = zip.files[name]; if (file.dir) { dir.push(pth + "/" + name); } else { files.push(name); } } if (dir.length > 0) { this.mkdirAll(dir) .then(() => { this.installExtension( files, zip ) .then(() => resolve()) .catch((e) => reject(__e(e)) ); }) .catch((e) => reject(__e(e))); } else { this.installExtension(files, zip) .then(() => resolve()) .catch((e) => reject(__e(e))); } }) .catch((e: Error) => reject(__e(e))); }) .catch((e) => reject(__e(e))); }) .catch((e) => reject(__e(e))); }); } } App.extensions.ExtensionMaker = ExtensionMaker; }