UNPKG

collider.jam

Version:

Hand-crafted and fun to use game framework for jamming, creative coding, and rapid game development

620 lines (528 loc) 18.3 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 TAG = 'scanner' let logLevel = 2 let scans = 0 let lastUnits function trace(msg) { if (logLevel < 2) return log.trace(msg, TAG) } function debug(msg) { if (logLevel === 0) return log.debug(msg, TAG) } function warn(msg) { if (logLevel < 2) return log.warn(msg, TAG) } function isIgnored(path) { if (lib.getResourceName(path).startsWith('.')) return true if (!env.scanMap.ignorePaths) return false for (let i = 0; i < env.scanMap.ignorePaths.length; i++) { const ipath = env.scanMap.ignorePaths[i] const irex = new RegExp(ipath) if (irex.test(path)) return true } return false /* return (path.endsWith('.DS_Store') || path.includes('.git') || path.endsWith('.out')) */ } function listFiles(unitPath, path, unit, onFile) { if (isIgnored(path)) { trace('ignoring X ' + lib.addPath(unitPath, path), TAG) return } else { trace('scanning ' + lib.addPath(unitPath, path), TAG) } fs.readdirSync(lib.addPath(unitPath, path)).forEach(entry => { const localPath = lib.addPath(path, entry) const fullPath = lib.addPath(unitPath, localPath) const lstat = fs.lstatSync(fullPath) // check on directory if (lstat.isDirectory()) { listFiles(unitPath, localPath, unit, onFile) } else { if (isIgnored(localPath)) { trace(' X ' + localPath + ' (ignored)') } else { if (onFile) { onFile(localPath, fullPath, lstat, unit) } else { trace(' ? ' + localPath) } } } }) } function loadOptionalJson(path) { if (fs.existsSync(path)) { trace('loading json: ' + path) return fs.readJsonSync(path) } } function loadOptionalList(path) { if (fs.existsSync(path)) { trace('loading list: ' + path) const data = fs.readFileSync(path, 'utf8') const list = data.split(/\r?\n/g) .map(e => e.trim()) .filter(e => e.length > 0) .filter(e => !e.startsWith('#')) return list } else { return [] } } function loadOptionalUnitConfig(path) { const config = loadOptionalJson(path) if (config) { debug('extending global config with: ' + path, TAG) _.extendOwn(env.config, config) } } 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 Unit = function(id, mix, type, path, requireMix) { trace('================================================') trace(`found ${type} [${id}]: ${path}`) this.id = id this.mix = mix this.type = type this.path = path this.requireMix = requireMix this.pak = loadOptionalJson(lib.addPath(path, env.pakConfig)) loadOptionalUnitConfig(lib.addPath(path, 'config.json')) this.ignore = loadOptionalList(lib.addPath(path, 'unit.ignore')) this.ls = [] this.diff = [] this.mtime = {} listFiles(path, '', this, (localPath, fullPath, lstat, unit) => { unit.ls.push(localPath) unit.mtime[localPath] = lstat.mtimeMs trace(' * ' + localPath) }) this.toString = function() { let s = 'unit/' + this.type + ' [' + this.id + ']\n' s += 'path: ' + this.path + '\n' s += this.ls.map(f => '* ' + f).join('\n') s += this.ignore.map(f => '- ' + f).join('\n') return s } } 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 in sketch mod mode') } else { debug('running in sketch mix mode') } // set sketch mod defaults // can be redefined later env.scanMap = lib.augment({}, env.sketchScanMap) debug('MODULES: ' + env.jamModules) if (env.jamModules) { env.scanMap.modules.push(env.jamModules) } else { warn("Can't determine collider.jam module path.") warn(`Set the path manually in modules: [] section by creating ./${env.mapConfig}.`) } } else { debug('running in package mode') env.scanMap = lib.augment({}, env.defaultScanMap) if (!env.globalMode) { env.jamPath = './node_modules/' + env.colliderPackage debug('switching to local modules: ' + env.jamPath) } } // try to read default unit structure from jam const jpath = lib.addPath(env.jamPath, 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.jamPath + '/' + 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.jamPath) 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('| 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 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') } debug('units found: ' + units.length) lastUnits = units scans ++ if (scans === 1) logLevel = 0 return units } function syncUnit(unit) { 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 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)) }) }, }