undo3d
Version:
Undo3D helps you build free-roaming 3D web apps where thousands of users can collaborate creatively in real time. Expect the first public beta mid-2019, and the first production release mid-2020.
256 lines (198 loc) • 9.64 kB
JavaScript
//// Quickly creates Undo3D source code and test files and folders, based on
//// this `definitions` object:
const definitions = {
// name, parentName, folder, description ... and `index` will be added automatically.
Base: [ null, 'base', 'The base class for all other classes.' ]
, App: [ 'Base', 'base', 'Represents a single Undo3D application.' ]
, Item: [ 'Base', 'base', 'Something listable: you can ‘B.R.E.A.D’ with it.' ]
, Module: [ 'Base', 'base', 'A discrete area of functionality, or ‘business logic’.' ]
, Ui: [ 'Base', 'base', 'A user-interface element.' ]
, Value: [ 'Base', 'base', 'An Item is composed of Values.' ]
, Panel: [ 'Ui', 'ui', 'A control panel, often for a Module.' ]
, Zone: [ 'Ui', 'ui', 'A section of the screen, containing Panels.' ]
, Form: [ 'Ui', 'ui', 'Edits most Values of one Item - sometimes more than one.' ]
, Input: [ 'Ui', 'ui', 'Edits one Value - sometimes more than one.' ]
, List: [ 'Ui', 'ui', 'An ordered sequence of Items, usually of the same type.' ]
, Row: [ 'Ui', 'ui', 'Edits a few Values of one Item.' ]
, Boot: [ 'Module', 'core/boot', 'Manages app and Module lifecycles.' ]
, Cli: [ 'Module', 'core/cli', 'Every app has a Command Line Interface - usually hidden.' ]
, Hub: [ 'Module', 'core/hub', 'The app’s nerve centre. Contains a list of Listeners.' ]
, Router: [ 'Module', 'core/router', 'Manages the app’s internal navigation.' ]
, State: [ 'Module', 'core/state', 'A List of Values, persisted in various ways.' ]
, Team: [ 'Module', 'core/team', 'The app’s set of Users. Minimally, just ‘anonymous’.' ]
, Logline: [ 'Item', 'core/cli', 'The record of a Cli command, or some other debug message.' ]
, Listener: [ 'Item', 'core/hub', 'A stored function, fired by the Hub.' ]
, Route: [ 'Item', 'core/router', 'Routes link URLs to Zone and Panel visibilities.' ]
, User: [ 'Item', 'core/team', 'Usually represents a person. Could be a robot though.' ]
, Debug: [ 'Zone', 'zone', 'Contains helpful developer tools. For example the Cli.' ]
, Footer: [ 'Zone', 'zone', 'Small, fullwidth, and at the end. Partly or fully sticky.' ]
, Header: [ 'Zone', 'zone', 'Small, fullwidth, and up top. Partly or fully sticky.' ]
, News: [ 'Zone', 'zone', 'Usually a dismissable list of notifications.' ]
, Popup: [ 'Zone', 'zone', 'A dialog or alert. Temporarily prevents UI interactions.' ]
, Primary: [ 'Zone', 'zone', 'Contains the main content, often a 3D scene.' ]
, Secondary:[ 'Zone', 'zone', 'The preferred place to display Forms.' ]
, Tertiary: [ 'Zone', 'zone', 'The preferred place to display Lists.' ]
}
//// EXPECTED ENVIRONMENT
//// Make sure we’re running in Node, and that the present working directory is
//// as expected.
if ('object' !== typeof process || 'function' !== typeof require)
throw Error('Run in Node.js')
if ('undo3d' !== process.cwd().split('/').pop() )
throw Error(`Run in ‘undo3d/’ not ‘${process.cwd().split('/').pop()}/’`)
//// DEFS
//// Convert `definitions`, an object of arrays, to `defs`, an array of objects.
//// This is mostly just to make the rest of the code clearer to read.
const defs = []
for (const name in definitions) {
const
definition = definitions[name]
, [ parentName, folder, description ] = definition
//// Validate.
if (! parentName && null !== parentName) throw Error(`definitions.${name} has no parentName`)
if (! folder) throw Error(`definitions.${name} has no folder`)
if (! description) throw Error(`definitions.${name} has no description`)
//// Automatically add an `index` to the definition. This will help us
//// cross-reference between `defs` and `definitions`.
definition.push(defs.length)
//// Create a new object in the `defs` array.
defs.push({
name
, parentName
, folder
, description
, lcName: name.toLowerCase() //@TODO validate unique
, parent: null
, children: []
, level: null
})
}
//// Validate the inheritance tree, and add `parent`.
for (const def of defs) {
const { name, parentName } = def
if (null === parentName) continue // the base class
if (! definitions[parentName])
throw Error(`No such definitions.${name} parent '${parentName}'`)
const index = definitions[parentName][3]
def.parent = defs[index]
}
//// Add `children` backreferences to defs which have them.
for (const def of defs) {
const { parent } = def
if (! parent) continue // the base class
parent.children.push(def)
}
//// Add the `level`.
for (const def of defs) {
const { name, parent } = def
def.level = ! parent ? 0 // the base class
: ! parent.parent ? 1
: ! parent.parent.parent ? 2
: ! parent.parent.parent.parent ? 3
: null
if (null === def.level) throw Error(`definitions.${name} inherits too deeply`)
}
//// Make a list of folders which may need to be created.
const tmpFolders = defs.map( def => def.folder )
for (const folder of tmpFolders) {
const parts = folder.split('/')
if (1 === parts.length) continue // is top-level
tmpFolders.push( parts.slice(0, -1).join('/') ) // all but the last part
}
const folders = [...new Set(tmpFolders)].sort()
//// MAKE FOLDERS
//// Load Node’s filesystem and path libraries.
const
fs = require('fs')
, path = require('path')
//// Create the outer ‘src/’ and ‘test/’ folders, if missing.
if (! fs.existsSync('./src') ) fs.mkdirSync('./src')
if (! fs.existsSync('./test') ) fs.mkdirSync('./test')
//// Create the inner folders, if missing.
for (const name of folders) {
if (! fs.existsSync('./src/'+name) ) fs.mkdirSync('./src/'+name)
if (! fs.existsSync('./test/'+name) ) fs.mkdirSync('./test/'+name)
}
//// MAKE FILES
//// Create the source and test files, if missing.
for (const def of defs) {
const
{ folder, lcName } = def
, src = `./src/${folder}/${lcName}.mjs`
, test = `./test/${folder}/${lcName}.test.mjs`
if (! fs.existsSync(src) ) fs.writeFileSync(src, makeSrc(def) )
if (! fs.existsSync(test) ) fs.writeFileSync(test, makeTest(def) )
}
//// Create ‘src/all.mjs’, if missing.
if (! fs.existsSync('./src/all.mjs') )
fs.writeFileSync('./src/all.mjs', makeAllSrc() )
//// Create ‘test/all.test.mjs’, if missing.
if (! fs.existsSync('./test/all.test.mjs') )
fs.writeFileSync('./test/all.test.mjs', makeAllTest() )
//// UTILITY
//// Returns autogenerated content for a source file.
function makeSrc (def) {
const
{ name, folder, description, parent} = def
, { name:pName, folder:pFolder, lcName:pLcName } = parent || {}
, tree = getArrowTree(def)
, rel = parent ? path.relative(folder, pFolder) : ''
, extender = parent ? `extends ${pName} ` : ''
, treePropValue = parent ? `${pName}.tree+' < ${name}'` : `'${name}'`
, out = [ `//// ${tree}`,`//// ${description}`, '' ]
if (parent) out.push(`import ${pName} from '${rel?rel:'.'}/${pLcName}.mjs'`,'')
out.push(`export default class ${name} ${extender}{`,'}','')
out.push('//// Static properties.',`Object.defineProperties(${name}, {`
, ` tree: { value:${treePropValue}, enumerable:true }`
, '})','')
return out.join('\n')
}
//// Returns autogenerated content for a test file.
function makeTest (def) {
const
{ name, folder, description, lcName, parent, level } = def
, { name:pName, folder:pFolder, lcName:pLcName } = parent || {}
, tree = getArrowTree(def)
, depth = folder.split('/').length + 1
, rel = '../'.repeat(depth) + 'src/' + folder
, out = [ `//// Unit test for ${tree}`, `//// ${description}`, '' ]
out.push(`import ${name} from '${rel}/${lcName}.mjs'`)
out.push(`import { strict as a } from '` + '../'.repeat(depth)
+ `deps/node_modules/undo3d-shim-browser/assert/all.mjs'`,'')
out.push(`a.equal('${tree}', ${name}.tree,`)
out.push(` \`${name}.tree should be '${tree}'\`)`,'')
return out.join('\n')
}
//// Returns autogenerated content for the ‘master’ test file.
function makeAllTest () {
const out = []
out.push(`//// Run all ${defs.length} Undo3D unit tests`,'')
for (const def of defs) {
const { folder, lcName } = def
out.push(`import './${folder}/${lcName}.test.mjs'`)
}
return out.join('\n')
}
//// Returns autogenerated content for the ‘master’ source file.
function makeAllSrc () {
const out = []
out.push(`//// Bundles all ${defs.length} classes, ready for injection into a new App.`,'')
for (const def of defs) {
const { name, folder, lcName } = def
out.push(`import ${name} from './${folder}/${lcName}.mjs'`)
}
out.push('','export default {')
out.push( ' ' + defs.map( def => def.name ).join('\n , ') )
out.push('}','')
return out.join('\n')
}
//// Returns a class’s inheritance tree, rendered with less-than angle brackets,
//// eg 'Base < Ui < Panel < Cli'
function getArrowTree (def) {
const
{ name, parent, level } = def
, tree = [ name ]
for (let l=0, p=parent; l<level; l++, p = p.parent)
tree.unshift(p.name)
return tree.join(' < ')
}