diff --git a/SupClient/index.ts b/SupClient/index.ts index 6efed532..437096c8 100644 --- a/SupClient/index.ts +++ b/SupClient/index.ts @@ -33,7 +33,7 @@ if ((global as any).SupApp == null) { } // Initialize empty system -SupCore.system = new SupCore.System("", ""); +SupCore.system = new SupCore.System("", "", ""); const plugins: { [contextName: string]: { [pluginName: string]: { path: string; content: any; } } } = {}; diff --git a/SupCore/SupCore.d.ts b/SupCore/SupCore.d.ts index 0fb45655..764add59 100644 --- a/SupCore/SupCore.d.ts +++ b/SupCore/SupCore.d.ts @@ -322,13 +322,14 @@ declare namespace SupCore { } class System { + systemPath: string; id: string; folderName: string; data: SystemData; pluginsInfo: PluginsInfo; serverBuild: (server: ProjectServer, buildPath: string, callback: (err: string) => void) => void; - constructor(id: string, folderName: string); + constructor(systemPath: string, id: string, folderName: string); requireForAllPlugins(filePath: string): void; registerPlugin(contextName: string, pluginName: string, plugin: T): void; getPlugins(contextName: string): { [pluginName: string]: T }; @@ -337,6 +338,7 @@ declare namespace SupCore { // All loaded systems (server-side only) export const systems: { [system: string]: System }; export const systemsPath: string; + export const rwSystemsPath: string; // The currently active system export let system: System; diff --git a/SupCore/index.ts b/SupCore/index.ts index 09f3cb6b..c69a2c9f 100644 --- a/SupCore/index.ts +++ b/SupCore/index.ts @@ -3,9 +3,11 @@ import * as Data from "./Data"; export { Data }; export let systemsPath: string; +export let rwSystemsPath: string; -export function setSystemsPath(path: string) { +export function setSystemsPath(path: string, rwPath: string) { systemsPath = path; + rwSystemsPath = rwPath; } export * from "./systems"; diff --git a/SupCore/systems.ts b/SupCore/systems.ts index d1128163..ad8c0566 100644 --- a/SupCore/systems.ts +++ b/SupCore/systems.ts @@ -12,12 +12,12 @@ export class System { pluginsInfo: SupCore.PluginsInfo; serverBuild: (server: ProjectServer, buildPath: string, callback: (err: string) => void) => void; - constructor(public id: string, public folderName: string) { + constructor(public systemPath: string, public id: string, public folderName: string) { this.data = new SystemData(this); } requireForAllPlugins(filePath: string) { - const pluginsPath = path.resolve(`${SupCore.systemsPath}/${this.folderName}/plugins`); + const pluginsPath = path.resolve(`${this.systemPath}/${this.folderName}/plugins`); for (const pluginAuthor of fs.readdirSync(pluginsPath)) { const pluginAuthorPath = `${pluginsPath}/${pluginAuthor}`; diff --git a/server/ProjectHub.ts b/server/ProjectHub.ts index 2f8a4435..0045ccf4 100644 --- a/server/ProjectHub.ts +++ b/server/ProjectHub.ts @@ -19,10 +19,10 @@ export default class ProjectHub { serversById: { [serverId: string]: ProjectServer } = {}; loadingProjectFolderName: string; - constructor(globalIO: SocketIO.Server, dataPath: string, callback: (err: Error) => any) { + constructor(globalIO: SocketIO.Server, dataPath: string, rwDataPath: string, callback: (err: Error) => any) { this.globalIO = globalIO; - this.projectsPath = path.join(dataPath, "projects"); - this.buildsPath = path.join(dataPath, "builds"); + this.projectsPath = path.join(rwDataPath, "projects"); + this.buildsPath = path.join(rwDataPath, "builds"); const serveProjects = (callback: ErrorCallback) => { async.eachSeries(fs.readdirSync(this.projectsPath), (folderName: string, cb: (err: Error) => any) => { diff --git a/server/RemoteHubClient.ts b/server/RemoteHubClient.ts index 5d5bbb01..6190b7f3 100644 --- a/server/RemoteHubClient.ts +++ b/server/RemoteHubClient.ts @@ -35,7 +35,10 @@ export default class RemoteHubClient extends BaseRemoteClient { let formatVersion = SupCore.Data.ProjectManifest.currentFormatVersion; let templatePath: string; if (details.template != null) { - templatePath = `${SupCore.systemsPath}/${details.systemId}/public/templates/${details.template}`; + templatePath = `${SupCore.rwSystemsPath}/${details.systemId}/public/templates/${details.template}`; + if (!fs.existsSync(templatePath)) { + templatePath = `${SupCore.systemsPath}/${details.systemId}/public/templates/${details.template}`; + } formatVersion = JSON.parse(fs.readFileSync(path.join(templatePath, `manifest.json`), { encoding: "utf8" })).formatVersion; } diff --git a/server/commands/start.ts b/server/commands/start.ts index 82ebcd00..87f63627 100644 --- a/server/commands/start.ts +++ b/server/commands/start.ts @@ -26,6 +26,7 @@ require("module").Module._initPaths(); /* tslint:enable */ let dataPath: string; +let rwDataPath: string; let hub: ProjectHub = null; let mainApp: express.Express = null; let mainHttpServer: http.Server; @@ -46,16 +47,17 @@ function onUncaughtException(err: Error) { process.exit(1); } -export default function start(serverDataPath: string) { +export default function start(serverDataPath: string, serverRwDataPath: string) { dataPath = serverDataPath; - SupCore.log(`Using data from ${dataPath}.`); + rwDataPath = serverRwDataPath; + SupCore.log(`Using data from ${dataPath} and ${rwDataPath}.`); process.on("uncaughtException", onUncaughtException); loadConfig(); const { version, superpowers: { appApiVersion: appApiVersion } } = JSON.parse(fs.readFileSync(`${__dirname}/../../package.json`, { encoding: "utf8" })); SupCore.log(`Server v${version} starting...`); - fs.writeFileSync(`${__dirname}/../../public/superpowers.json`, JSON.stringify({ + fs.writeFileSync(`${rwDataPath}/superpowers.json`, JSON.stringify({ serverName: config.server.serverName, version, appApiVersion, hasPassword: config.server.password.length !== 0 @@ -63,7 +65,7 @@ export default function start(serverDataPath: string) { // SupCore (global as any).SupCore = SupCore; - SupCore.setSystemsPath(path.join(dataPath, "systems")); + SupCore.setSystemsPath(path.join(dataPath, "systems"), path.join(rwDataPath, "systems")); // List available languages languageIds = fs.readdirSync(`${__dirname}/../../public/locales`); @@ -77,7 +79,7 @@ export default function start(serverDataPath: string) { memoryStore = new expressSession.MemoryStore(); try { - const sessionsJSON = fs.readFileSync(`${__dirname}/../../sessions.json`, { encoding: "utf8" }); + const sessionsJSON = fs.readFileSync(`${rwDataPath}/sessions.json`, { encoding: "utf8" }); (memoryStore as any).sessions = JSON.parse(sessionsJSON); } catch (err) { // Ignore @@ -112,6 +114,7 @@ export default function start(serverDataPath: string) { mainApp.get("/serverBuild", enforceAuth, serveServerBuildIndex); mainApp.use("/projects/:projectId/*", serveProjectWildcard); + mainApp.use("/superpowers.json", express.static(`${rwDataPath}/superpowers.json`)); mainApp.use("/", express.static(`${__dirname}/../../public`)); mainHttpServer = http.createServer(mainApp); @@ -131,6 +134,7 @@ export default function start(serverDataPath: string) { buildApp.get("/", redirectToHub); buildApp.get("/systems/:systemId/SupCore.js", serveSystemSupCore); + buildApp.use("/superpowers.json", express.static(`${rwDataPath}/superpowers.json`)); buildApp.use("/", express.static(`${__dirname}/../../public`)); buildApp.use((req, res, next) => { @@ -166,7 +170,7 @@ export default function start(serverDataPath: string) { function loadConfig() { let mustWriteConfig = false; - const serverConfigPath = `${dataPath}/config.json`; + const serverConfigPath = `${rwDataPath}/config.json`; if (fs.existsSync(serverConfigPath)) { config.setServerConfig(JSON.parse(fs.readFileSync(serverConfigPath, { encoding: "utf8" }))); schemas.validate(config, "config"); @@ -277,7 +281,7 @@ function onSystemsLoaded() { buildApp.use(handle404); // Project hub - hub = new ProjectHub(io, dataPath, (err: Error) => { + hub = new ProjectHub(io, dataPath, rwDataPath, (err: Error) => { if (err != null) { SupCore.log(`Failed to start server:\n${(err as any).stack}`); return; } SupCore.log(`Loaded ${Object.keys(hub.serversById).length} projects from ${hub.projectsPath}.`); @@ -323,7 +327,7 @@ function onExit() { SupCore.log("Saving sessions..."); try { const sessionsJSON = JSON.stringify((memoryStore as any).sessions, null, 2); - fs.writeFileSync(`${__dirname}/../../sessions.json`, sessionsJSON); + fs.writeFileSync(`${rwDataPath}/sessions.json`, sessionsJSON); } catch (err) { SupCore.log(`Failed to save sessions:\n${(err as any).stack}`); hadError = true; diff --git a/server/commands/utils.ts b/server/commands/utils.ts index 60c24e61..5621d996 100644 --- a/server/commands/utils.ts +++ b/server/commands/utils.ts @@ -16,16 +16,19 @@ export const pluginNameRegex = /^[A-Za-z0-9]+\/[A-Za-z0-9]+$/; // Data path const argv = yargs - .describe("data-path", "Path to store/read data files from, including config and projects") + .describe("data-path", "Path to read data files from") + .describe("rw-data-path", "Path to store/read data files from, including config and projects") .describe("download-url", "Url to download a release") .boolean("force") .argv; export const dataPath = argv["data-path"] != null ? path.resolve(argv["data-path"]) : path.resolve(`${__dirname}/../..`); -mkdirp.sync(dataPath); -mkdirp.sync(`${dataPath}/projects`); -mkdirp.sync(`${dataPath}/builds`); +export const rwDataPath = argv["rw-data-path"] != null ? path.resolve(argv["rw-data-path"]) : path.resolve(`${__dirname}/../..`); +mkdirp.sync(rwDataPath); +mkdirp.sync(`${rwDataPath}/projects`); +mkdirp.sync(`${rwDataPath}/builds`); export const systemsPath = `${dataPath}/systems`; -mkdirp.sync(systemsPath); +export const rwSystemsPath = `${rwDataPath}/systems`; +mkdirp.sync(rwSystemsPath); export const force = argv["force"]; export const downloadURL = argv["download-url"]; @@ -39,56 +42,62 @@ export const systemsById: { [id: string]: { plugins: { [authorName: string]: { [pluginName: string]: { version: string; isDev: boolean; } } }; } } = {}; -for (const entry of fs.readdirSync(systemsPath)) { - if (!folderNameRegex.test(entry)) continue; - if (!fs.statSync(`${systemsPath}/${entry}`).isDirectory) continue; - - let systemId: string; - let systemVersion: string; - const systemPath = `${systemsPath}/${entry}`; - try { - const packageDataFile = fs.readFileSync(`${systemPath}/package.json`, { encoding: "utf8" }); - const packageData = JSON.parse(packageDataFile); - systemId = packageData.superpowers.systemId; - systemVersion = packageData.version; - } catch (err) { - emitError(`Could not load system id from systems/${entry}/package.json:`, err.stack); - } +const allSystemsPaths = [`${rwSystemsPath}`, `${systemsPath}`]; - let isDev = true; - try { if (!fs.lstatSync(`${systemPath}/.git`).isDirectory()) isDev = false; } - catch (err) { isDev = false; } - - systemsById[systemId] = { folderName: entry, version: systemVersion, isDev, plugins: {} }; - let pluginAuthors: string[]; - try { pluginAuthors = fs.readdirSync(`${systemPath}/plugins`); } catch (err) { /* Ignore */ } - if (pluginAuthors == null) continue; - - for (const pluginAuthor of pluginAuthors) { - if (builtInPluginAuthors.indexOf(pluginAuthor) !== -1) continue; - if (!folderNameRegex.test(pluginAuthor)) continue; - - systemsById[systemId].plugins[pluginAuthor] = {}; - for (const pluginName of fs.readdirSync(`${systemPath}/plugins/${pluginAuthor}`)) { - if (!folderNameRegex.test(pluginName)) continue; - - const pluginPath = `${systemPath}/plugins/${pluginAuthor}/${pluginName}`; - if (!fs.statSync(pluginPath).isDirectory) continue; - - let pluginVersion: string; - try { - const packageDataFile = fs.readFileSync(`${pluginPath}/package.json`, { encoding: "utf8" }); - const packageData = JSON.parse(packageDataFile); - pluginVersion = packageData.version; - } catch (err) { - emitError(`Could not load plugin verson from systems/${entry}/${pluginAuthor}/${pluginName}/package.json:`, err.stack); - } +for (const sysPath of allSystemsPaths) { + for (const entry of fs.readdirSync(sysPath)) { + if (!folderNameRegex.test(entry)) continue; + + const systemPath = `${sysPath}/${entry}`; + if (!fs.existsSync(`${systemPath}/package.json`)) continue; + + let systemId: string; + let systemVersion: string; + try { + const packageDataFile = fs.readFileSync(`${systemPath}/package.json`, { encoding: "utf8" }); + const packageData = JSON.parse(packageDataFile); + systemId = packageData.superpowers.systemId; + systemVersion = packageData.version; + } catch (err) { + emitError(`Could not load system id from systems/${entry}/package.json:`, err.stack); + } + + let isDev = true; + try { if (!fs.lstatSync(`${systemPath}/.git`).isDirectory()) isDev = false; } + catch (err) { isDev = false; } - let isDev = true; - try { if (!fs.lstatSync(`${pluginPath}/.git`).isDirectory()) isDev = false; } - catch (err) { isDev = false; } + if (systemId in systemsById) { continue; } + systemsById[systemId] = { folderName: entry, version: systemVersion, isDev, plugins: {} }; + let pluginAuthors: string[]; + try { pluginAuthors = fs.readdirSync(`${systemPath}/plugins`); } catch (err) { /* Ignore */ } + if (pluginAuthors == null) continue; - systemsById[systemId].plugins[pluginAuthor][pluginName] = { version: pluginVersion, isDev }; + for (const pluginAuthor of pluginAuthors) { + if (builtInPluginAuthors.indexOf(pluginAuthor) !== -1) continue; + if (!folderNameRegex.test(pluginAuthor)) continue; + + systemsById[systemId].plugins[pluginAuthor] = {}; + for (const pluginName of fs.readdirSync(`${systemPath}/plugins/${pluginAuthor}`)) { + if (!folderNameRegex.test(pluginName)) continue; + + const pluginPath = `${systemPath}/plugins/${pluginAuthor}/${pluginName}`; + if (!fs.statSync(pluginPath).isDirectory) continue; + + let pluginVersion: string; + try { + const packageDataFile = fs.readFileSync(`${pluginPath}/package.json`, { encoding: "utf8" }); + const packageData = JSON.parse(packageDataFile); + pluginVersion = packageData.version; + } catch (err) { + emitError(`Could not load plugin verson from systems/${entry}/${pluginAuthor}/${pluginName}/package.json:`, err.stack); + } + + let isDev = true; + try { if (!fs.lstatSync(`${pluginPath}/.git`).isDirectory()) isDev = false; } + catch (err) { isDev = false; } + + systemsById[systemId].plugins[pluginAuthor][pluginName] = { version: pluginVersion, isDev }; + } } } } diff --git a/server/index.ts b/server/index.ts index 46f0a930..d89f5bba 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,13 +1,14 @@ /// import * as yargs from "yargs"; -import { dataPath } from "./commands/utils"; +import { dataPath, rwDataPath } from "./commands/utils"; // Command line interface const argv = yargs .usage("Usage: $0 [options]") .demand(1, "Enter a command") - .describe("data-path", "Path to store/read data files from, including config and projects") + .describe("data-path", "Path to read data files from") + .describe("rw-data-path", "Path to store/read data files from, including config and projects") .command("start", "Start the server", (yargs) => { yargs.demand(1, 1, `The "start" command doesn't accept any arguments`).argv; }) @@ -33,7 +34,7 @@ const command = argv._[0]; const [ systemId, pluginFullName ] = argv._[1] != null ? argv._[1].split(":") : [ null, null ]; switch (command) { /* tslint:disable */ - case "start": require("./commands/start").default(dataPath); break; + case "start": require("./commands/start").default(dataPath, rwDataPath); break; case "registry": require("./commands/registry").default(); break; case "install": require("./commands/install").default(systemId, pluginFullName); break; case "uninstall": require("./commands/uninstall").default(systemId, pluginFullName); break; diff --git a/server/loadSystems.ts b/server/loadSystems.ts index 35c61dc4..cd8e5318 100644 --- a/server/loadSystems.ts +++ b/server/loadSystems.ts @@ -2,22 +2,35 @@ import * as path from "path"; import * as fs from "fs"; import * as express from "express"; import * as async from "async"; +import * as mkdirp from "mkdirp"; import getLocalizedFilename from "./getLocalizedFilename"; function shouldIgnoreFolder(pluginName: string) { return pluginName.indexOf(".") !== -1 || pluginName === "node_modules"; } export default function(mainApp: express.Express, buildApp: express.Express, callback: Function) { - async.eachSeries(fs.readdirSync(SupCore.systemsPath), (systemFolderName, cb) => { + const rwDirs = fs.readdirSync(SupCore.rwSystemsPath).map((systemFolderName) => { + return [SupCore.rwSystemsPath, systemFolderName]; + }); + const dirs = fs.readdirSync(SupCore.systemsPath).map((systemFolderName) => { + return [SupCore.systemsPath, systemFolderName]; + }); + async.eachSeries(rwDirs.concat(dirs), (pathAndFolderName, cb) => { + const systemFolderName = pathAndFolderName[1]; if (systemFolderName.indexOf(".") !== -1) { cb(); return; } - const systemPath = path.join(SupCore.systemsPath, systemFolderName); + const systemsPath = pathAndFolderName[0]; + const systemPath = path.join(systemsPath, systemFolderName); + const rwSystemPath = path.join(SupCore.rwSystemsPath, systemFolderName); if (!fs.statSync(systemPath).isDirectory()) { cb(); return; } + const pkgJSONFile = path.join(systemPath, "package.json"); + if (!fs.existsSync(pkgJSONFile)) { cb(); return; } - const systemId = JSON.parse(fs.readFileSync(path.join(SupCore.systemsPath, systemFolderName, "package.json"), { encoding: "utf8" })).superpowers.systemId; - SupCore.system = SupCore.systems[systemId] = new SupCore.System(systemId, systemFolderName); + const systemId = JSON.parse(fs.readFileSync(pkgJSONFile, { encoding: "utf8" })).superpowers.systemId; + if (systemId in SupCore.systems) { cb(); return; } + SupCore.system = SupCore.systems[systemId] = new SupCore.System(systemsPath, systemId, systemFolderName); // Expose public stuff - try { fs.mkdirSync(`${systemPath}/public`); } catch (err) { /* Ignore */ } + try { fs.mkdirSync(`${rwSystemPath}/public`); } catch (err) { /* Ignore */ } mainApp.use(`/systems/${systemId}`, express.static(`${systemPath}/public`)); buildApp.use(`/systems/${systemId}`, express.static(`${systemPath}/public`)); @@ -25,7 +38,12 @@ export default function(mainApp: express.Express, buildApp: express.Express, cal let templatesList: string[] = []; const templatesFolder = `${systemPath}/public/templates`; if (fs.existsSync(templatesFolder)) templatesList = fs.readdirSync(templatesFolder); - fs.writeFileSync(`${systemPath}/public/templates.json`, JSON.stringify(templatesList, null, 2)); + const systemPublicDir = `${rwSystemPath}/public`; + mkdirp.sync(systemPublicDir); + const templatesFile = `${systemPublicDir}/templates.json`; + fs.writeFileSync(templatesFile, JSON.stringify(templatesList, null, 2)); + mainApp.use(`/systems/${systemId}/templates.json`, express.static(templatesFile)); + buildApp.use(`/systems/${systemId}/templates.json`, express.static(templatesFile)); // Load server-side system module const systemServerModulePath = `${systemPath}/server`; @@ -37,12 +55,18 @@ export default function(mainApp: express.Express, buildApp: express.Express, cal // Load plugins const pluginsInfo = SupCore.system.pluginsInfo = loadPlugins(systemId, `${systemPath}/plugins`, mainApp, buildApp); - fs.writeFileSync(`${systemPath}/public/plugins.json`, JSON.stringify(pluginsInfo, null, 2)); + const pluginsFile = `${systemPublicDir}/plugins.json`; + fs.writeFileSync(pluginsFile, JSON.stringify(pluginsInfo, null, 2)); + mainApp.use(`/systems/${systemId}/plugins.json`, express.static(pluginsFile)); + buildApp.use(`/systems/${systemId}/plugins.json`, express.static(pluginsFile)); cb(); }, () => { const systemsInfo: SupCore.SystemsInfo = { list: Object.keys(SupCore.systems) }; - fs.writeFileSync(`${__dirname}/../public/systems.json`, JSON.stringify(systemsInfo, null, 2)); + const systemsFile = `${SupCore.rwSystemsPath}/systems.json`; + fs.writeFileSync(systemsFile, JSON.stringify(systemsInfo, null, 2)); + mainApp.use("/systems.json", express.static(systemsFile)); + buildApp.use("/systems.json", express.static(systemsFile)); SupCore.system = null; callback();