diff --git a/player/src/index.ts b/player/src/index.ts
index f439112..06e1cef 100644
--- a/player/src/index.ts
+++ b/player/src/index.ts
@@ -2,11 +2,13 @@
///
import * as querystring from "querystring";
-let qs: { project: string; build: string; } = querystring.parse(window.location.search.slice(1));
+const qs: { project: string; build: string; } = querystring.parse(window.location.search.slice(1));
+
+const indexPath = `/builds/${qs.project}/${qs.build}/files/index.html`;
if ((window as any).SupApp != null) {
- SupApp.openLink(`${window.location.origin}/builds/${qs.project}/${qs.build}/index.html`);
+ SupApp.openLink(`${window.location.origin}${indexPath}`);
window.close();
} else {
- window.location.href = `/builds/${qs.project}/${qs.build}/index.html`;
+ window.location.href = indexPath;
}
diff --git a/plugins/default/blob/data/BlobAsset.ts b/plugins/default/blob/data/BlobAsset.ts
index e5e535f..2a1f817 100644
--- a/plugins/default/blob/data/BlobAsset.ts
+++ b/plugins/default/blob/data/BlobAsset.ts
@@ -79,8 +79,8 @@ export default class BlobAsset extends SupCore.Data.Base.Asset {
});
}
- serverExport(buildPath: string, assetsById: { [id: string]: BlobAsset }, callback: (err: Error) => void) {
- if (this.pub.buffer == null) { callback (null); return; }
+ serverExport(buildPath: string, assetsById: { [id: string]: BlobAsset }, callback: (err: Error, writtenFiles: string[]) => void) {
+ if (this.pub.buffer == null) { callback (null, []); return; }
let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".") <= pathFromId.lastIndexOf("/")) {
@@ -90,7 +90,11 @@ export default class BlobAsset extends SupCore.Data.Base.Asset {
let outputPath = `${buildPath}/${pathFromId}`;
let parentPath = outputPath.slice(0, outputPath.lastIndexOf("/"));
- mkdirp(parentPath, () => { fs.writeFile(outputPath, this.pub.buffer, callback); });
+ mkdirp(parentPath, () => {
+ fs.writeFile(outputPath, this.pub.buffer, (err) => {
+ callback(err, [ pathFromId ]);
+ });
+ });
}
server_upload(client: any, mediaType: string, buffer: Buffer, callback: UploadCallback) {
diff --git a/plugins/default/export/build/WebBuildSettings.d.ts b/plugins/default/export/build/WebBuildSettings.d.ts
new file mode 100644
index 0000000..11161cf
--- /dev/null
+++ b/plugins/default/export/build/WebBuildSettings.d.ts
@@ -0,0 +1,3 @@
+interface WebBuildSettings {
+ outputFolder: string;
+}
diff --git a/plugins/default/export/build/WebBuildSettingsEditor.ts b/plugins/default/export/build/WebBuildSettingsEditor.ts
new file mode 100644
index 0000000..b904140
--- /dev/null
+++ b/plugins/default/export/build/WebBuildSettingsEditor.ts
@@ -0,0 +1,96 @@
+import * as TreeView from "dnd-tree-view";
+
+let outputFolder: string;
+
+export default class WebBuildSettingsEditor implements SupClient.BuildSettingsEditor {
+ private outputFolderTextfield: HTMLInputElement;
+ private outputFolderButton: HTMLButtonElement;
+ private errorRowElt: HTMLTableRowElement;
+ private errorInput: HTMLInputElement;
+
+ private table: HTMLTableElement;
+
+ constructor(container: HTMLDivElement, private entries: SupCore.Data.Entries, private entriesTreeView: TreeView) {
+ const { table, tbody } = SupClient.table.createTable(container);
+ this.table = table;
+ table.classList.add("properties");
+ table.hidden = true;
+
+ const outputFolderRow = SupClient.table.appendRow(tbody, SupClient.i18n.t("buildSettingsEditors:web.outputFolder"));
+ const inputs = SupClient.html("div", "inputs", { parent: outputFolderRow.valueCell });
+
+ const value = outputFolder != null ? outputFolder : "";
+ this.outputFolderTextfield = SupClient.html("input", { parent: inputs, type: "text", value, readOnly: true, style: { cursor: "pointer" } }) as HTMLInputElement;
+ this.outputFolderButton = SupClient.html("button", { parent: inputs, textContent: SupClient.i18n.t("common:actions.select") }) as HTMLButtonElement;
+
+ this.outputFolderTextfield.addEventListener("click", (event) => { event.preventDefault(); this.selectOutputfolder(); });
+ this.outputFolderButton.addEventListener("click", (event) => { event.preventDefault(); this.selectOutputfolder(); });
+
+ const errorRow = SupClient.table.appendRow(tbody, "Error");
+ this.errorRowElt = errorRow.row;
+ this.errorRowElt.hidden = true;
+ this.errorInput = SupClient.html("input", { parent: errorRow.valueCell, type: "text", readOnly: true }) as HTMLInputElement;
+ }
+
+ setVisible(visible: boolean) {
+ this.table.hidden = !visible;
+ }
+
+ getSettings(callback: (settings: WebBuildSettings) => void) {
+ this.ensureOutputFolderValid((outputFolderValid) => {
+ callback(outputFolderValid ? { outputFolder } : null);
+ });
+ }
+
+ private selectOutputfolder() {
+ SupApp.chooseFolder((folderPath) => {
+ if (folderPath == null) return;
+
+ outputFolder = this.outputFolderTextfield.value = folderPath;
+ this.ensureOutputFolderValid();
+ });
+ }
+
+ private ensureOutputFolderValid(callback?: (outputFolderValid: boolean) => void) {
+ if (outputFolder == null) {
+ this.displayError(SupClient.i18n.t("buildSettingsEditors:web.errors.selectDestionationFolder"));
+ if (callback != null) callback(false);
+ return;
+ }
+
+ SupApp.readDir(outputFolder, (err, files) => {
+ if (err != null) {
+ this.displayError(SupClient.i18n.t("buildSettingsEditors:web.errors.emptyDirectoryCheckFail"));
+ console.log(err);
+ if (callback != null) callback(false);
+ return;
+ }
+
+ let index = 0;
+ while (index < files.length) {
+ const item = files[index];
+ if (item[0] === "." || item === "Thumbs.db") files.splice(index, 1);
+ else index++;
+ }
+
+ if (files.length > 0) {
+ this.displayError(SupClient.i18n.t("buildSettingsEditors:web.errors.destinationFolderEmpty"));
+ if (callback != null) callback(false);
+ } else {
+ this.errorRowElt.hidden = true;
+ if (callback != null) callback(true);
+ }
+ });
+ }
+
+ private displayError(err: string) {
+ this.errorRowElt.hidden = false;
+ this.errorInput.value = err;
+
+ (this.errorRowElt as any).animate([
+ { transform: "translateX(0)" },
+ { transform: "translateX(1.5vh)" },
+ { transform: "translateX(0)" }
+ ], { duration: 100 });
+ }
+}
diff --git a/plugins/default/export/build/buildWeb.ts b/plugins/default/export/build/buildWeb.ts
new file mode 100644
index 0000000..d36096c
--- /dev/null
+++ b/plugins/default/export/build/buildWeb.ts
@@ -0,0 +1,72 @@
+import * as async from "async";
+import * as querystring from "querystring";
+import supFetch from "../../../../../../SupClient/src/fetch";
+import * as path from "path";
+
+const qs = querystring.parse(window.location.search.slice(1));
+
+let settings: WebBuildSettings;
+
+const progress = { index: 0, total: 0, errors: 0 };
+const statusElt = document.querySelector(".status");
+const progressElt = document.querySelector("progress") as HTMLProgressElement;
+const detailsListElt = document.querySelector(".details ol") as HTMLOListElement;
+
+export default function build(socket: SocketIOClient.Socket, theSettings: WebBuildSettings, buildPort: number) {
+ settings = theSettings;
+
+ socket.emit("build:project", (err: string, buildId: string) => {
+ const buildPath = `${window.location.protocol}//${window.location.hostname}:${buildPort}/builds/${qs.project}/${buildId}/`;
+
+ supFetch(`${buildPath}files.json`, "json", (err, filesToDownload) => {
+ if (err != null) {
+ progress.errors++;
+ SupClient.html("li", { parent: detailsListElt, textContent: SupClient.i18n.t("builds:web.errors.exportFailed", { path: settings.outputFolder }) });
+ return;
+ }
+
+ progress.total = filesToDownload.length;
+ updateProgress();
+
+ async.each(filesToDownload as string[], (filePath, cb) => {
+ downloadFile(buildPath, filePath, (err) => {
+ if (err != null) {
+ progress.errors++;
+ SupClient.html("li", { parent: detailsListElt, textContent: SupClient.i18n.t("builds:web.errors.exportFailed", { path: filePath }) });
+ } else {
+ progress.index++;
+ updateProgress();
+ }
+
+ cb(err);
+ });
+ });
+ });
+ });
+}
+
+function updateProgress() {
+ progressElt.max = progress.total;
+ progressElt.value = progress.index;
+
+ if (progress.index < progress.total) {
+ statusElt.textContent = SupClient.i18n.t("builds:web.progress", { path: settings.outputFolder, index: progress.index, total: progress.total });
+ } else if (progress.errors > 0) {
+ statusElt.textContent = SupClient.i18n.t("builds:web.doneWithErrors", { path: settings.outputFolder, total: progress.total, errors: progress.errors });
+ } else {
+ statusElt.textContent = SupClient.i18n.t("builds:web.done", { path: settings.outputFolder, total: progress.total });
+ }
+}
+
+function downloadFile(buildPath: string, filePath: string, callback: ErrorCallback) {
+ const inputPath = `${buildPath}files/${filePath}`;
+ const outputPath = path.join(settings.outputFolder, filePath);
+
+ SupApp.mkdirp(path.dirname(outputPath), (err) => {
+ supFetch(inputPath, "arraybuffer", (err, data) => {
+ if (err != null) { callback(err); return; }
+
+ SupApp.writeFile(outputPath, new Buffer(data), callback);
+ });
+ });
+}
diff --git a/plugins/default/export/build/index.ts b/plugins/default/export/build/index.ts
new file mode 100644
index 0000000..29fa66b
--- /dev/null
+++ b/plugins/default/export/build/index.ts
@@ -0,0 +1,9 @@
+///
+
+import WebBuildSettingsEditor from "./WebBuildSettingsEditor";
+import buildWeb from "./buildWeb";
+
+SupClient.registerPlugin("build", "web", {
+ settingsEditor: WebBuildSettingsEditor,
+ build: buildWeb
+});
diff --git a/plugins/default/export/index.d.ts b/plugins/default/export/index.d.ts
new file mode 100644
index 0000000..d215d34
--- /dev/null
+++ b/plugins/default/export/index.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/plugins/default/export/package.json b/plugins/default/export/package.json
new file mode 100644
index 0000000..d224f11
--- /dev/null
+++ b/plugins/default/export/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "superpowers-web-default-export-plugin",
+ "description": "Export plugin for Superpowers Web, the collaborative static site editor",
+ "version": "1.0.0",
+ "license": "ISC",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/superpowers/superpowers-web.git"
+ },
+ "scripts": {
+ "build": "gulp --gulpfile=../../../../../scripts/pluginGulpfile.js --cwd=."
+ }
+}
diff --git a/plugins/default/export/public/locales/en/buildSettingsEditors.json b/plugins/default/export/public/locales/en/buildSettingsEditors.json
new file mode 100644
index 0000000..b72924f
--- /dev/null
+++ b/plugins/default/export/public/locales/en/buildSettingsEditors.json
@@ -0,0 +1,13 @@
+{
+ "web": {
+ "label": "Default",
+ "description": "Export as HTML5",
+
+ "outputFolder": "Output folder",
+ "errors": {
+ "selectDestionationFolder": "Select a destination folder.",
+ "emptyDirectoryCheckFail": "Failed to check if the selected folder is empty.",
+ "destinationFolderEmpty": "The destination folder must be empty."
+ }
+ }
+}
diff --git a/plugins/default/export/public/locales/en/builds.json b/plugins/default/export/public/locales/en/builds.json
new file mode 100644
index 0000000..45b7302
--- /dev/null
+++ b/plugins/default/export/public/locales/en/builds.json
@@ -0,0 +1,12 @@
+{
+ "web": {
+ "title": "HTML5 export",
+ "progress": "Exporting to ${path}... (${index}/${total})",
+ "doneWithErrors": "Encountered ${errors} errors while exporting ${total} elements to ${path}.",
+ "done": "Exported ${total} elements to ${path}.",
+
+ "errors": {
+ "exportFailed": "Failed to export ${path}."
+ }
+ }
+}
diff --git a/plugins/default/export/tsconfig.json b/plugins/default/export/tsconfig.json
new file mode 100644
index 0000000..d322f2a
--- /dev/null
+++ b/plugins/default/export/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "noImplicitAny": true
+ }
+}
diff --git a/plugins/default/jade/data/JadeAsset.ts b/plugins/default/jade/data/JadeAsset.ts
index 05f83d9..9317df6 100644
--- a/plugins/default/jade/data/JadeAsset.ts
+++ b/plugins/default/jade/data/JadeAsset.ts
@@ -96,7 +96,7 @@ export default class JadeAsset extends SupCore.Data.Base.Asset {
});
}
- serverExport(buildPath: string, assetsById: { [id: string]: JadeAsset }, callback: (err: Error) => void) {
+ serverExport(buildPath: string, assetsById: { [id: string]: JadeAsset }, callback: (err: Error, writtenFiles: string[]) => void) {
let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".jade") === pathFromId.length - 5) pathFromId = pathFromId.slice(0, -5);
let outputPath = `${buildPath}/${pathFromId}.html`;
@@ -139,7 +139,11 @@ export default class JadeAsset extends SupCore.Data.Base.Asset {
}
fs.readFileSync = oldReadFileSync;
- mkdirp(parentPath, () => { fs.writeFile(outputPath, html, callback); });
+ mkdirp(parentPath, () => {
+ fs.writeFile(outputPath, html, (err) => {
+ callback(err, [ `${pathFromId}.html` ]);
+ });
+ });
}
server_editText(client: any, operationData: OperationData, revisionIndex: number, callback: EditTextCallback) {
diff --git a/plugins/default/json/data/JSONAsset.ts b/plugins/default/json/data/JSONAsset.ts
index a493247..ae53318 100644
--- a/plugins/default/json/data/JSONAsset.ts
+++ b/plugins/default/json/data/JSONAsset.ts
@@ -89,14 +89,18 @@ export default class JSONAsset extends SupCore.Data.Base.Asset {
});
}
- serverExport(buildPath: string, assetsById: { [id: string]: JSONAsset }, callback: (err: Error) => void) {
+ serverExport(buildPath: string, assetsById: { [id: string]: JSONAsset }, callback: (err: Error, writtenFiles: string[]) => void) {
let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".json") === pathFromId.length - 5) pathFromId = pathFromId.slice(0, -5);
let outputPath = `${buildPath}/${pathFromId}.json`;
let parentPath = outputPath.slice(0, outputPath.lastIndexOf("/"));
let text = this.pub.text;
- mkdirp(parentPath, () => { fs.writeFile(outputPath, text, callback); });
+ mkdirp(parentPath, () => {
+ fs.writeFile(outputPath, text, (err) => {
+ callback(err, [ `${pathFromId}.json` ]);
+ });
+ });
}
server_editText(client: any, operationData: OperationData, revisionIndex: number, callback: EditTextCallback) {
diff --git a/plugins/default/stylus/data/StylusAsset.ts b/plugins/default/stylus/data/StylusAsset.ts
index d1c1930..96344b3 100644
--- a/plugins/default/stylus/data/StylusAsset.ts
+++ b/plugins/default/stylus/data/StylusAsset.ts
@@ -94,7 +94,7 @@ export default class StylusAsset extends SupCore.Data.Base.Asset {
});
}
- serverExport(buildPath: string, assetsById: { [id: string]: StylusAsset }, callback: (err: Error) => void) {
+ serverExport(buildPath: string, assetsById: { [id: string]: StylusAsset }, callback: (err: Error, writtenFiles: string[]) => void) {
let pathFromId = this.server.data.entries.getPathFromId(this.id);
if (pathFromId.lastIndexOf(".styl") === pathFromId.length - 5) pathFromId = pathFromId.slice(0, -5);
let outputPath = `${buildPath}/${pathFromId}.css`;
@@ -116,7 +116,11 @@ export default class StylusAsset extends SupCore.Data.Base.Asset {
};
let css = stylus(this.pub.text).set("filename", `${pathFromId}.styl`).set("cache", false).render();
fs.readFileSync = oldReadFileSync;
- mkdirp(parentPath, () => { fs.writeFile(outputPath, css, callback); });
+ mkdirp(parentPath, () => {
+ fs.writeFile(outputPath, css, (err) => {
+ callback(err, [ `${pathFromId}.css` ]);
+ });
+ });
}
server_editText(client: any, operationData: OperationData, revisionIndex: number, callback: EditTextCallback) {
diff --git a/server/index.ts b/server/index.ts
index 2793ca8..ea270b4 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -1,25 +1,29 @@
///
import * as async from "async";
+import * as fs from "fs";
-interface ExportableAsset extends SupCore.Data.Base.Asset {
- serverExport: (outputPath: string, assetsById: { [id: string]: ExportableAsset }, callback: (err: Error) => void) => void;
+interface ServerExportableAsset extends SupCore.Data.Base.Asset {
+ serverExport: (outputPath: string, assetsById: { [id: string]: ServerExportableAsset }, callback: (err: Error, writtenFiles: string[]) => void) => void;
}
SupCore.system.serverBuild = (server: ProjectServer, buildPath: string, callback: (err: string) => void) => {
const assetIdsToExport: string[] = [];
+ let files: string[] = [];
server.data.entries.walk((entry: SupCore.Data.EntryNode, parent: SupCore.Data.EntryNode) => {
- if (entry.type != null) assetIdsToExport.push(entry.id);
+ if (entry.type == null) return;
+
+ assetIdsToExport.push(entry.id);
});
- const assetsById: { [id: string]: ExportableAsset } = {};
+ const assetsById: { [id: string]: ServerExportableAsset } = {};
async.series([
// Acquire all assets
(cb) => {
async.each(assetIdsToExport, (assetId, cb) => {
- server.data.assets.acquire(assetId, null, (err: Error, asset: ExportableAsset) => {
+ server.data.assets.acquire(assetId, null, (err: Error, asset: ServerExportableAsset) => {
assetsById[assetId] = asset;
cb();
});
@@ -29,7 +33,8 @@ SupCore.system.serverBuild = (server: ProjectServer, buildPath: string, callback
// Export all assets
(cb) => {
async.each(assetIdsToExport, (assetId, cb) => {
- assetsById[assetId].serverExport(buildPath, assetsById, () => {
+ assetsById[assetId].serverExport(`${buildPath}/files`, assetsById, (err, writtenFiles) => {
+ files = files.concat(writtenFiles);
cb();
});
}, cb);
@@ -39,6 +44,7 @@ SupCore.system.serverBuild = (server: ProjectServer, buildPath: string, callback
// Release all assets
for (const assetId of assetIdsToExport) server.data.assets.release(assetId, null);
- callback(null);
+ // Write files.json
+ fs.writeFile(`${buildPath}/files.json`, JSON.stringify(files), callback);
});
};