UNPKG

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
//// 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(' < ') }