diff --git a/packages/server/utilities/builder/buildApp.js b/packages/server/utilities/builder/buildApp.js new file mode 100644 index 0000000000..f2791c29f8 --- /dev/null +++ b/packages/server/utilities/builder/buildApp.js @@ -0,0 +1,134 @@ +const { + appPackageFolder +} = require("../createAppPackage"); +const { componentLibraryInfo } = require("./componentLibraryInfo"); +const { + stat, ensureDir, pathExists, + constants, copyFile, writeFile, + readFile +} = require("fs-extra"); +const { join, resolve, dirname } = require("path"); +const sqrl = require('squirrelly'); + +module.exports = async (config, appname, pages, appdefinition) => { + + const appPath = appPackageFolder(config, appname); + + await buildClientAppDefinition( + config, appname, + appdefinition, + appPath, pages, "main"); + + await buildClientAppDefinition( + config, appname, + appdefinition, + appPath, pages, "unauthenticated") + + await buildIndexHtml( + config, appname, appPath, + pages, "main"); + + await buildIndexHtml( + config, appname, appPath, + pages, "unauthenticated"); + + await copyClientLib(appPath, "main"); + await copyClientLib(appPath, "unauthenticated"); + +} + +const publicPath = (appPath, pageName) => join(appPath, "public", pageName); +const rootPath = (config, appname) => config.useAppRootPath ? `/${appname}` : ""; + +const copyClientLib = async (appPath, pageName) => { + var sourcepath = require.resolve("@budibase/client"); + var destPath = join(publicPath(appPath, pageName), "budibase-client.js"); + + await copyFile(sourcepath, destPath, constants.COPYFILE_FICLONE); + await copyFile(sourcepath + ".map", destPath + ".map", constants.COPYFILE_FICLONE); + +} + +const buildIndexHtml = async (config, appname, appPath, pages, pageName) => { + + + const appPublicPath = publicPath(appPath, pageName); + const appRootPath = rootPath(config, appname); + + const stylesheetUrl = s => + s.indexOf('http://') === 0 || s.indexOf('https://') === 0 + ? s + : `/${rootPath(config, appname)}/${s}`; + + const templateObj = { + title: pages[pageName].index.title || "Budibase App", + favicon: `${appRootPath}/${pages[pageName].index.favicon || "/_shared/favicon.png"}`, + stylesheets: (pages.stylesheets || []).map(stylesheetUrl), + appRootPath + } + + const indexHtmlTemplate = await readFile( + resolve(__dirname, "index.template.html"), "utf8"); + + const indexHtmlPath = join(appPublicPath, "index.html"); + + const indexHtml = sqrl.Render(indexHtmlTemplate, templateObj) + + await writeFile( + indexHtmlPath, + indexHtml, + {flag:"w+"}); + +} + + +const buildClientAppDefinition = async (config, appname, appdefinition, appPath, pages, pageName) => { + + + const appPublicPath = publicPath(appPath, pageName); + const appRootPath = rootPath(config, appname); + + + const componentLibraries = []; + + for(let lib of pages.componentLibraries) { + const info = await componentLibraryInfo(appPath, lib); + const libFile = info.components._lib || "index.js"; + const source = join(info.libDir, libFile); + const moduleDir = join(appPublicPath, "lib", info.libDir.replace(appPath, "")); + const destPath = join(moduleDir, libFile); + + await ensureDir(dirname(destPath)); + + componentLibraries.push({ + importPath: destPath.replace(appPublicPath, "") + .replace(/\\/g, "/"), + libName: lib + }); + + let shouldCopy = !(await pathExists(destPath)); + if(!shouldCopy) { + const destStat = await stat(destPath); + const sourceStat = await stat(source); + shouldCopy = destStat.ctimeMs !== sourceStat.ctimeMs; + } + + if(shouldCopy) { + await copyFile(source, destPath, constants.COPYFILE_FICLONE); + } + + } + + const filename = join(appPublicPath, "clientAppDefinition.js"); + + const clientAppDefObj = { + hierarchy: appdefinition.hierarchy, + componentLibraries: componentLibraries, + appRootPath: appRootPath, + props: appdefinition.props[pageName] + } + + await writeFile(filename, + `window['##BUDIBASE_APPDEFINITION##'] = ${JSON.stringify(clientAppDefObj)}`); + +} \ No newline at end of file diff --git a/packages/server/utilities/builder/componentLibraryInfo.js b/packages/server/utilities/builder/componentLibraryInfo.js new file mode 100644 index 0000000000..64998ad3f3 --- /dev/null +++ b/packages/server/utilities/builder/componentLibraryInfo.js @@ -0,0 +1,67 @@ +const { + readJSON, exists +} = require("fs-extra"); +const { + resolve, join , dirname +} = require("path"); + +const getLibDir = (appPath, libname) => { + try { + const componentsFile = require.resolve( + join(libname, "components.json"), + { paths: [appPath]}); + return dirname(componentsFile); + } catch(e) { + console.log(e); + } +} + +const getComponentsFilepath = libPath => + resolve(libPath, "components.json"); + +module.exports.componentsFilepath = (appPath, libname) => + getComponentsFilepath(getLibDir(appPath, libname)); + +module.exports.componentLibraryInfo = async (appPath, libname) => { + + const libDir = getLibDir(appPath, libname); + const componentsPath = getComponentsFilepath(libDir); + + if(!await exists(componentsPath)) { + const e = new Error(`could not find components definition file at ${componentsPath}`); + e.statusCode = 404; + throw e; + } + + try { + const components = await readJSON(componentsPath); + const namespacedComponents = {_lib:components._lib}; + for(let cname in components) { + if(cname === "_lib" || cname == "_generators") continue; + const namespacedName = `${libname}/${cname}`; + components[cname].name = namespacedName; + namespacedComponents[namespacedName] = components[cname]; + } + + const namespacedGenerators = {} + if(components._generators) { + namespacedGenerators._lib=components._generators._lib || "generators.js"; + for(let gname in components._generators) { + if(gname === "_lib") continue; + const namespacedName = `${libname}/${gname}`; + components._generators[gname].name = namespacedName; + namespacedGenerators[namespacedName] = components._generators[gname]; + } + } + + return ({ + components: namespacedComponents, + generators: namespacedGenerators, + libDir, + componentsPath + }); + } catch(e) { + const err = `could not parse JSON - ${componentsPath} : ${e.message}`; + throw new Error(err); + } +} \ No newline at end of file diff --git a/packages/server/utilities/builder/index.js b/packages/server/utilities/builder/index.js new file mode 100644 index 0000000000..4829e663e8 --- /dev/null +++ b/packages/server/utilities/builder/index.js @@ -0,0 +1,175 @@ +const { + appPackageFolder, + appsFolder +} = require("../createAppPackage"); +const { + readJSON, writeJSON, readdir, + stat, ensureDir, rename, + unlink, rmdir +} = require("fs-extra"); +const { + join,dirname +} = require("path"); +const { $ } = require("@budibase/core").common; +const { + keyBy, intersection, map +} = require("lodash/fp"); +const {merge} = require("lodash"); + +const { componentLibraryInfo } = require("./componentLibraryInfo"); +const savePackage = require("./savePackage"); +const buildApp = require("./buildApp"); + +module.exports.savePackage = savePackage; + +const getPages = async (appPath) => await readJSON(`${appPath}/pages.json`); +const getAppDefinition = async (appPath) => await readJSON(`${appPath}/appDefinition.json`); + +module.exports.getPackageForBuilder = async (config, appname) => { + const appPath = appPackageFolder(config, appname); + + const pages = await getPages(appPath); + + return ({ + appDefinition: await getAppDefinition(appPath), + + accessLevels: await readJSON(`${appPath}/access_levels.json`), + + pages, + + components: await getComponents(appPath, pages), + + screens: keyBy("name")( + await fetchscreens(appPath)) + }); + +} + + + +module.exports.getApps = async (config, master) => { + const dirs = await readdir(appsFolder(config)); + + return $(master.listApplications(), [ + map(a => a.name), + intersection(dirs) + ]); +} + + +const componentPath = (appPath, name) => + join(appPath, "components", name + ".json"); + +module.exports.saveScreen = async (config, appname, component) => { + const appPath = appPackageFolder(config, appname); + const compPath = componentPath(appPath, component.name); + await ensureDir(dirname(compPath)); + await writeJSON( + compPath, + component, + {encoding:"utf8", flag:"w", spaces:2}); +} + +module.exports.renameScreen = async (config, appname, oldName, newName) => { + const appPath = appPackageFolder(config, appname); + + const oldComponentPath = componentPath( + appPath, oldName); + + const newComponentPath = componentPath( + appPath, newName); + + await ensureDir(dirname(newComponentPath)); + await rename( + oldComponentPath, + newComponentPath); +} + +module.exports.deleteScreen = async (config, appname, name) => { + const appPath = appPackageFolder(config, appname); + const componentFile = componentPath(appPath, name); + await unlink(componentFile); + + const dir = dirname(componentFile); + if((await readdir(dir)).length === 0) { + await rmdir(dir); + } +} + +module.exports.componentLibraryInfo = async (config, appname, lib) => { + const appPath = appPackageFolder(config, appname); + return await componentLibraryInfo(appPath, lib); +}; + + +const getComponents = async (appPath, pages ,lib) => { + + let libs; + if(!lib) { + pages = pages || await readJSON( + `${appPath}/pages.json`); + + if(!pages.componentLibraries) return []; + + libs = pages.componentLibraries; + } else { + libs = [lib]; + } + + const components = {}; + const generators = {}; + + for(let l of libs) { + const info = await componentLibraryInfo(appPath, l); + merge(components, info.components); + merge(generators, info.generators); + } + + if(components._lib) delete components._lib; + if(components._generators) delete components._generators; + + return {components, generators}; +} + +const fetchscreens = async (appPath, relativePath = "") => { + + const currentDir = join(appPath, "components", relativePath); + + const contents = await readdir(currentDir); + + const components = []; + + for(let item of contents) { + const itemRelativePath = join(relativePath, item); + const itemFullPath = join(currentDir, item); + const stats = await stat(itemFullPath); + + if(stats.isFile()) { + + if(!item.endsWith(".json")) continue; + + const component = + await readJSON(itemFullPath); + + component.name = itemRelativePath + .substring(0, itemRelativePath.length - 5) + .replace(/\\/g, "/"); + + component.props = component.props || {}; + + components.push(component); + } else { + const childComponents = await fetchscreens( + appPath, join(relativePath, item) + ); + + for(let c of childComponents) { + components.push(c); + } + } + } + + return components; +} + +module.exports.getComponents = getComponents; \ No newline at end of file diff --git a/packages/server/utilities/builder/index.template.html b/packages/server/utilities/builder/index.template.html new file mode 100644 index 0000000000..dcaa22cfe4 --- /dev/null +++ b/packages/server/utilities/builder/index.template.html @@ -0,0 +1,32 @@ + + + + + + + {{ title }} + + + + + {{ each(options.stylesheets) }} + + {{ /each }} + + + + + + + + + + + \ No newline at end of file diff --git a/packages/server/utilities/builder/savePackage.js b/packages/server/utilities/builder/savePackage.js new file mode 100644 index 0000000000..a9d41a295b --- /dev/null +++ b/packages/server/utilities/builder/savePackage.js @@ -0,0 +1,25 @@ +const { appPackageFolder } = require("../createAppPackage"); +const { writeJSON } = require("fs-extra"); +const buildApp = require("./buildApp"); + +module.exports = async (config, appname, pkg) => { + const appPath = appPackageFolder(config, appname); + await writeJSON( + `${appPath}/appDefinition.json`, + pkg.appDefinition, + {spaces:2}); + + await writeJSON( + `${appPath}/access_levels.json`, + pkg.accessLevels, + {spaces:2}); + + await writeJSON( + `${appPath}/pages.json`, + pkg.pages, + {spaces:2}); + + await buildApp( + config, appname, + pkg.pages, pkg.appDefinition); +} \ No newline at end of file