UNPKG

collider.jam

Version:

Hand-crafted game framework for jamming, creative coding and rapid indie game development

548 lines (462 loc) 16.4 kB
'use strict' const fs = require('fs-extra') const _ = require('underscore') const env = require('./env') const log = require('./log') const lib = require('./lib') const flow = require('./flow') const { loadOptionalJson, listFiles } = require('./loader') const Unit = require('./dna/Unit') const TAG = 'scanner' let scans = 0 let lastUnits function trace(msg) { if (log.level < 2) return log.trace(msg, TAG) } function debug(msg) { if (log.level === 0) return log.debug(msg, TAG) } function warn(msg) { if (log.level < 2) return log.warn(msg, TAG) } function scanPackageDependencies(mix, packageJson) { if (!packageJson || !_.isObject(packageJson.dependencies)) return let ls = [] Object.keys(packageJson.dependencies).forEach(d => { if (d.endsWith('mix') || d.endsWith('mod') || d.endsWith('fix')) { if (d !== mix) ls.push(d) } }) return ls.length > 0? ls : undefined } const UnitMap = function() { this.length = 0 this.units = {} this.opt = {} this.map = {} this.mix = {} this.sample = {} this.sampleList = [] this.patch = {} } UnitMap.prototype.register = function(unit) { let units = this.units if (unit.pak && unit.pak.optional) { // unit defined as optional if (env.scanMap.include && env.scanMap.include.includes(unit.id)) { debug(`including optional [${unit.id}] according to the config`) } else { debug(`skipping [${unit.id}]` + `- include explicitly in ${env.pakConfig} if needed`) units = this.opt } } if (units[unit.id]) { //log.dump(env.scanMap) warn(`unit [${unit.id}] at: ${unit.path}`) warn(`is replacing unit ${this.units[unit.id].path}`) warn(`could be a problem with ${env.mapConfig}`) warn('figure out if that is what you really want') warn(`if not, check [scanner] logs and [${env.scanMap.origin}]`) //throw 'unit mapped to [' + unit.id + '] already exists!' } else { this.length ++ } units[unit.id] = unit // map mix to unit if (unit.type !== 'pub') { if (!this.mix[unit.mix]) this.mix[unit.mix] = [] this.mix[unit.mix].push(unit.id) } } UnitMap.prototype.registerSample = function(sample) { // TODO what to do with possible name collisions? const info = lib.readOptionalJson( lib.addPath(sample.path, 'info.json') ) lib.augment(sample, info) this.sample[sample.name] = sample this.sample[sample.id] = sample this.sampleList.push(sample) } UnitMap.prototype.registerPatch = function(patch) { // TODO what to do with possible name collisions? this.patch[patch.name] = patch this.patch[patch.id] = patch } UnitMap.prototype.generateMap = function() { const __ = this Object.values(this.units).forEach(unit => { if (unit.requireMix) { // resolve requirements from the mix unit.require = [] unit.requireMix.forEach(mixId => { const mixUnits = __.mix[mixId] if (mixUnits) unit.require = unit.require.concat(mixUnits) }) } // only mods and fixes are added to the map if (unit.type !== 'pub') { const unitMap = { type: unit.type, mix: unit.mix, require: unit.require, ls: unit.ls, ignore: unit.ignore, } __.map[unit.id] = _.extendOwn(unitMap, unit.pak) } }) return this } UnitMap.prototype.forEach = function(fn) { Object.values(this.units).forEach(fn) } function includePath(units, mix, fullPath, entry, requireMix) { if (entry.endsWith('.pub') || entry === 'pub') { units.register(new Unit(mix, mix, 'pub', fullPath, requireMix)) } else if (entry.endsWith('.fix') || entry === 'fix') { units.register(new Unit(lib.addPath(mix, entry), mix, 'fix', fullPath, requireMix)) } else if (entry.endsWith('.mod') || entry === 'mod') { units.register(new Unit(lib.addPath(mix, entry), mix, 'mod', fullPath, requireMix)) } else if (entry.endsWith('.sample')) { units.registerSample({ id: lib.addPath(mix, entry), name: entry.substring(0, entry.length-7), mix: mix, type: 'sample', path: fullPath, }) } else if (entry.endsWith('.patch')) { units.registerPatch({ id: lib.addPath(mix, entry), name: entry.substring(0, entry.length-6), mix: mix, type: 'patch', path: fullPath, }) } else if (entry === '') { units.register(new Unit(mix, mix, 'mod', fullPath, requireMix)) } } const scanMix = function(units, path, mix) { if (!mix) mix = '' const packageJson = loadOptionalJson(lib.addPath(path, 'package.json')) const requireMix = scanPackageDependencies(mix, packageJson) fs.readdirSync(path).forEach(entry => { const fullPath = lib.addPath(path, entry) const lstat = fs.lstatSync(fullPath) if (lstat.isDirectory()) { includePath(units, mix, fullPath, entry, requireMix) } }) } let scanModules = function(units, path) { try { fs.readdirSync(path).forEach(entry => { let fullPath = lib.addPath(path, entry) let lstat = fs.lstatSync(fullPath) if (lstat.isDirectory() || lstat.isSymbolicLink()) { if (entry.endsWith('.mix')) { trace('================================================') trace(`found a mix [${fullPath}]`) // TODO dev/release includes should be configurable if (env.config.release && fullPath.includes('dev.mix')) { trace('ignoring for release: ' + fullPath) } else { scanMix(units, fullPath, entry) } } } }) // also scan for mods and fixes scanMix(units, path) } catch (e) { log.error(e, TAG) } } function tryToReadScanMap(path, defaultScanMap) { const scanMap = lib.readOptionalJson(path, undefined, () => debug(`found [${env.mapConfig}] at: ${path}`)) if (scanMap) { scanMap.origin = path return scanMap } else { return defaultScanMap } } function augmentArray(target, source, name) { const tar = target[name] || [] if (source[name]) { target[name] = tar.concat(source[name]) } } function replaceArray(target, source, name) { if (source[name]) { target[name] = source[name] } } function remap(path, scanMap) { const remap = lib.readOptionalJson(path, undefined, () => debug(`found [${env.remapConfig}] at: ${path}`)) if (remap) { //lib.augment(scanMap, remap) replaceArray(scanMap, remap, 'units') replaceArray(scanMap, remap, 'mixes') replaceArray(scanMap, remap, 'modules') replaceArray(scanMap, remap, 'include') replaceArray(scanMap, remap, 'ignore') replaceArray(scanMap, remap, 'ignorePaths') } } function determineScanMap() { if (env.scanMap) return env.scanMap if (env.sketch) { if (env.mode === env.MOD_MODE) { debug('running as a sketch mod') } else { debug('running as a sketch mix') } // set sketch mod defaults // can be redefined later env.scanMap = lib.augment({}, env.sketchScanMap) if (env.JAM_MODULES != undefined) { const paths = env.JAM_MODULES.split(':').filter(path => path) env.scanMap.modules.concat(paths) } else if (env.jamModules) { env.scanMap.modules.push(env.jamModules) } else { warn("Can't determine collider.jam module path.") warn(`Define colon-separated paths to collider modules in the JAM_MODULES environment variable,`) warn(`or set the path manually in modules: [] section by creating ./${env.mapConfig}.`) } } else { debug('running as a package mode') env.scanMap = lib.augment({}, env.defaultScanMap) if (!env.globalMode) { env.jamHome = './node_modules/' + env.colliderPackage debug('switching to local modules: ' + env.jamHome) } } // try to read default unit structure from jam const jpath = lib.addPath(env.jamHome, env.mapConfig) env.scanMap = tryToReadScanMap(jpath, env.scanMap) /* env.scanMap = lib.readOptionalJson(jpath, env.scanMap, () => debug(`using ${env.mapConfig} from: ${jpath}`)) */ // try to read unit structure from local project env.scanMap = tryToReadScanMap(env.mapConfig, env.scanMap) remap(env.jamHome + '/' + env.remapConfig, env.scanMap) remap(env.remapConfig, env.scanMap) //env.scanMap = lib.readOptionalJson(env.mapConfig, env.scanMap, // () => debug(`using local ./${env.mapConfig}`)) if (env.sketch) { if (!env.scanMap.mixes) env.scanMap.mixes = [] env.scanMap.mixes.push(env.jamHome) if (env.mode === env.MIX_MODE) { env.scanMap.mixes.push('./') } } else { if (!env.scanMap.mixes) env.scanMap.mixes = [] env.scanMap.mixes.push('') } return env.scanMap } function dumpScanMap() { const map = env.scanMap debug('==============================') debug('| SCAN MAP |') debug('==============================') if (map.modules && map.modules.length > 0) { debug('=== module paths ===') map.modules.forEach(path => debug(`* [${path}]`)) } if (map.mixes && map.mixes.length > 0) { debug('=== mixes paths ===') map.mixes.forEach(path => debug(`* [${path}]`)) } if (map.units && map.units.length > 0) { debug('=== units included ===') map.units.forEach(path => debug(`* [${path}]`)) } if (map.include && map.include.length > 0) { debug('=== include ===') map.include.forEach(path => debug(`* [${path}]`)) } if (map.ignore && map.ignore.length > 0) { debug('=== ignore ===') map.ignore.forEach(path => debug(`* [${path}]`)) } if (map.ignorePaths && map.ignorePaths.length > 0) { debug('=== ignore paths ===') map.ignorePaths.forEach(path => debug(`* [${path}]`)) } } function scanUnits() { if (!env.headless) lib.verifyBaseDir() const base = env.baseDir const scanMap = determineScanMap() debug(`using ${env.mapConfig} from: [${scanMap.origin}]`) trace('scanning the environment for collider.jam units...') dumpScanMap() const units = new UnitMap() if (_.isArray(scanMap.modules)) scanMap.modules.forEach(path => { if (path === undefined) return debug(`looking for units in [${path}]`) scanModules(units, lib.formPath(base, path)) }) if (_.isArray(scanMap.mixes)) scanMap.mixes.forEach(path => { if (path === undefined) return debug(`looking for units in mix [${path}]`) scanMix(units, lib.formPath(base, path)) }) if (_.isArray(scanMap.units)) scanMap.units.forEach(unit => { if (!unit || !unit.path) return unit.id = unit.id || '' debug(`including unit: [${unit.id}][${unit.path}]`) const fullPath = lib.formPath(base, unit.path) const i = fullPath.lastIndexOf('/') let entry = fullPath if (i >= 0) { entry = fullPath.substring(i+1) } entry = unit.name || entry includePath(units, unit.id, fullPath, entry) /* if (path === undefined) return debug(`including unit: [${path}]`) const fullPath = lib.formPath(base, path) const i = fullPath.lastIndexOf('/') let entry = fullPath if (i >= 0) { entry = fullPath.substring(i+1) } includePath(units, '', fullPath, entry) */ }) if (env.sketch && env.mode === env.MOD_MODE && !env.freezeScanMap) { // need to include ./ manually, // since ./ is remapped to /mod trace('================================================') debug(`map patch: including sketch mod at [./]`) includePath(units, '', './', 'mod') } if (env.monoLab) { try { //includePath(units, '', './', 'mod') const modUnit = new Unit('monoLib', '', 'fix', './', null, { skipScan: true }) const files = env.monoLab.split(',') files.forEach(f => { if (!fs.existsSync(f)) throw `can't find the file [${f}]` const stat = fs.statSync(f) if (!stat.isFile()) throw `[${f}] is expected to be a file` modUnit.addFile(f) }) modUnit.pak = { load: 'auto', mount: '/lab', fixMode: 'augment', } units.register(modUnit) } catch (e) { log.error(e, TAG) } } debug('units found: ' + units.length) lastUnits = units scans ++ if (scans === 1) log.level = 0 return units } function syncUnit(unit) { if (unit.opt && unit.opt.skipScan) return listFiles(unit.path, '', unit, function (path, fullPath, lstat, unit) { // TODO replace with actual ignore config if (path.endsWith('.DS_Store')) return const lastTime = unit.mtime[path] if (!lastTime || lstat.mtimeMs > lastTime) { // we've got updated file here! log.trace('[sync] ' + fullPath) unit.mtime[path] = lstat.mtimeMs unit.diff.push(path) flow.notify(`${unit.id}:${unit.path}:${path}`) } if (!lastTime) { // got a new file! register and notify //if (!unit.opt || !unit.opt.skipScan) { unit.ls.push(path) //} } // TODO add actual ignore config } ) /* unit.ls.forEach(path => { const fullPath = lib.addPath(unit.path, path) const lstat = fs.lstatSync(fullPath) const lastTime = unit.mtime[path] if (lstat.mtimeMs > lastTime) { // we've got updated file here! log.trace('[sync] ' + fullPath, TAG) unit.mtime[path] = lstat.mtimeMs unit.diff.push(path) flow.notify(`${unit.id}:${unit.path}:${path}`) } }) */ } function syncUnits() { Object.values(lastUnits.units).forEach(u => syncUnit(u)) } module.exports = { // scan collider project structure scan: function() { return scanUnits().generateMap() }, sync: function() { if (!lastUnits) this.scan() syncUnits() }, // scan a single unit structure rescanUnit: function(unit) { const map = {} const u = new Unit(unit.id, unit.type, unit.path, unit.require) map[u.id] = { type: u.type, mix: u.mix, pak: u.pak, require: unit.require, ls: u.ls, } return map }, printUnits: function() { const unitMap = scanUnits() Object.values(unitMap.units).forEach(unit => { log.raw( `${unit.type}:[${unit.id}] -- ` + `${unit.path} [${unit.ls.length} files]` ) }) Object.values(unitMap.opt).forEach(unit => { log.raw(`X ${unit.type}:[${unit.id}] -- ` + `${unit.path} [${unit.ls.length} files] (skipped)`) }) }, printFiles: function() { const unitMap = scanUnits() Object.values(unitMap.units).forEach(unit => { log.raw(`= ${unit.type}:[${unit.id}] -- ${unit.path}`) unit.ls.forEach(f => log.raw('* ' + f)) unit.ignore.forEach(f => log.raw('- ' + f)) }) }, printEnv: function() { const unitMap = scanUnits() console.dir(env) }, }