From 073028f07faf1eba5e19951064ffb7656e5f41db Mon Sep 17 00:00:00 2001 From: hoontee Date: Wed, 15 Nov 2023 00:30:01 -0600 Subject: [PATCH] Automated Jobs --- Lync/index.js | 250 ++++++++++++++++------------ Lync/output.js | 5 +- Lync/validator/project.js | 99 ++++++++--- README.md | 2 +- Sample Project/default.project.json | 9 + 5 files changed, 235 insertions(+), 130 deletions(-) diff --git a/Lync/index.js b/Lync/index.js index a5d178f..1da15fb 100644 --- a/Lync/index.js +++ b/Lync/index.js @@ -55,18 +55,19 @@ const CONFIG_PATH = path.resolve(LYNC_INSTALL_DIR, 'lync-config.json') let CONFIG; try { CONFIG = { - 'Debug': false, - 'GenerateSourcemap': true, - 'GithubAccessToken': '', - 'AutoUpdate': false, - 'AutoUpdate_UsePrereleases': false, - 'AutoUpdate_Repo': 'Iron-Stag-Games/Lync', - 'AutoUpdate_LatestId': 0, - 'Path_RobloxVersions': '', - 'Path_RobloxContent': '', - 'Path_RobloxPlugins': '', - 'Path_StudioModManagerContent': '', - 'Path_Lune': 'lune' + Debug: false, + GenerateSourcemap: true, + GithubAccessToken: '', + AutoUpdate: false, + AutoUpdate_UsePrereleases: false, + AutoUpdate_Repo: 'Iron-Stag-Games/Lync', + AutoUpdate_LatestId: 0, + Path_RobloxVersions: '', + Path_RobloxContent: '', + Path_RobloxPlugins: '', + Path_StudioModManagerContent: '', + Path_Lune: 'lune', + JobCommands: {} } if (PLATFORM == 'windows') { CONFIG.Path_RobloxVersions = '%LOCALAPPDATA%/Roblox/Versions' @@ -145,12 +146,12 @@ const USE_REMOTE = ARGS[2] && ARGS[2].toLowerCase() == 'remote' // Unimplemented const securityKeys = {} const mTimes = {} +var firstMapped = false var map = {} var modified = {} var modified_playtest = {} var modified_sourcemap = {} var projectJson; -var globIgnorePaths; var globIgnorePathsPicoMatch; var hardLinkPaths; @@ -302,6 +303,9 @@ function assignMap(robloxPath, mapDetails, mtimeMs) { modified_sourcemap[robloxPath] = mapDetails if (localPath) mTimes[localPath] = mtimeMs if (mapDetails.Meta) mTimes[mapDetails.Meta] = fs.statSync(mapDetails.Meta).mtimeMs // Meta File stats are never retrieved before this, so they aren't in a function parameter + if (MODE != 'build' && !firstMapped && localPath) { + runJobs('start', localPath) + } } /** @@ -792,11 +796,45 @@ async function changedJson() { map = {} const projectJsonStats = fs.statSync(PROJECT_JSON) for (const service in projectJson.tree) { - if (service == '$className') continue // Fix for Roblox LSP source map await mapJsonRecursive(PROJECT_JSON, projectJson.tree, 'tree', service, false, undefined, projectJsonStats.mtimeMs) } } +//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +// Automated Job Functions +//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +/** + * @param {string} event + * @param {string} localPath + * @param {boolean?} afterSync + */ +function runJobs(event, localPath, afterSync) { + if ('jobs' in projectJson) { + for (const index in projectJson.jobs) { + const job = projectJson.jobs[index] + if ((event == 'start' || job.afterSync == afterSync) && job.on.includes(event)) { + for (const path of job.globPaths) { + if (picomatch(path)(localPath.replace(/\\/g, '/'))) { + console.log('Running job command', green(job.commandName)) + if (job.commandName in CONFIG.JobCommands) { + spawn(CONFIG.JobCommands[job.commandName], [], { + stdio: 'ignore', + detached: true, + shell: true, + windowsHide: true + }) + } else { + console.error(fileError(CONFIG_PATH), yellow("Missing job command"), green(job.commandName)) + } + break + } + } + } + } + } +} + //------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ // Main //------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ @@ -889,6 +927,7 @@ async function changedJson() { // Map project await changedJson() + firstMapped = true // Download sources if (MODE == 'fetch') { @@ -1065,121 +1104,124 @@ async function changedJson() { cwd: process.cwd(), disableGlobbing: true, ignoreInitial: true, - ignored: globIgnorePaths, persistent: true, ignorePermissionErrors: true, alwaysStat: true, usePolling: true }).on('all', async function(event, localPath, localPathStats) { - if (DEBUG) console.log('E', yellow(event), cyan(localPath)) - try { - if (localPath) { - localPath = path.relative(process.cwd(), localPath) + runJobs(event, localPath, false) + if (!globIgnorePathsPicoMatch(localPath.replace(/\\/g, '/'))) { + if (DEBUG) console.log('E', yellow(event), cyan(localPath)) + try { + if (localPath) { + localPath = path.relative(process.cwd(), localPath) - if (!localPathIsIgnored(localPath)) { - localPath = localPath.replace(/\\/g, '/') - const parentPathString = path.relative(process.cwd(), path.resolve(localPath, '..')).replace(/\\/g, '/') + if (!localPathIsIgnored(localPath)) { + localPath = localPath.replace(/\\/g, '/') + const parentPathString = path.relative(process.cwd(), path.resolve(localPath, '..')).replace(/\\/g, '/') - if (localPath in mTimes) { - - // Deleted - if (!localPathStats) { - console.log('D', cyan(localPath)) - for (const key in map) { - - // Direct - if (map[key].Path && (map[key].Path == localPath || map[key].Path.startsWith(localPath + '/'))) { - delete mTimes[localPath] - delete mTimes[map[key].Path] - delete map[key] - modified[key] = false - modified_playtest[key] = false - modified_sourcemap[key] = false - if (localPathIsInit(localPath) && fs.existsSync(parentPathString)) { - await mapDirectory(parentPathString, key, 'Modified') + if (localPath in mTimes) { + + // Deleted + if (!localPathStats) { + console.log('D', cyan(localPath)) + for (const key in map) { + + // Direct + if (map[key].Path && (map[key].Path == localPath || map[key].Path.startsWith(localPath + '/'))) { + delete mTimes[localPath] + delete mTimes[map[key].Path] + delete map[key] + modified[key] = false + modified_playtest[key] = false + modified_sourcemap[key] = false + if (localPathIsInit(localPath) && fs.existsSync(parentPathString)) { + await mapDirectory(parentPathString, key, 'Modified') + } } - } - - // Meta - if (key in map && map[key].Meta && (map[key].Meta == localPath || map[key].Meta.startsWith(localPath + '/'))) { - if (fs.existsSync(map[key].Path)) { - await mapDirectory(map[key].Path, key, 'Modified') + + // Meta + if (key in map && map[key].Meta && (map[key].Meta == localPath || map[key].Meta.startsWith(localPath + '/'))) { + if (fs.existsSync(map[key].Path)) { + await mapDirectory(map[key].Path, key, 'Modified') + } + } + + // JSON member + if (key in map && map[key].ProjectJson == localPath) { + if (fs.existsSync(map[key].Path)) { + await mapDirectory(map[key].Path, key, 'Modified') + } } } - - // JSON member - if (key in map && map[key].ProjectJson == localPath) { - if (fs.existsSync(map[key].Path)) { + + // Changed + } else if (localPathStats.isFile() && mTimes[localPath] != localPathStats.mtimeMs) { + console.log('M', cyan(localPath)) + for (const key in map) { + if (map[key].InitParent == parentPathString) { + await mapDirectory(parentPathString, key, 'Modified') + } else if (map[key].Meta == localPath) { await mapDirectory(map[key].Path, key, 'Modified') + } else if (map[key].Path == localPath) { + await mapDirectory(localPath, key, 'Modified') } } + mTimes[localPath] = localPathStats.mtimeMs } - - // Changed - } else if (localPathStats.isFile() && mTimes[localPath] != localPathStats.mtimeMs) { - console.log('M', cyan(localPath)) - for (const key in map) { - if (map[key].InitParent == parentPathString) { - await mapDirectory(parentPathString, key, 'Modified') - } else if (map[key].Meta == localPath) { - await mapDirectory(map[key].Path, key, 'Modified') - } else if (map[key].Path == localPath) { - await mapDirectory(localPath, key, 'Modified') - } - } - mTimes[localPath] = localPathStats.mtimeMs - } - - } else if ((event == 'add' | event == 'addDir') && localPathStats) { - - // Added - if (parentPathString in mTimes && (!localPathStats.isFile() || localPathExtensionIsMappable(localPath))) { - console.log('A', cyan(localPath)) - for (const key in map) { - if (map[key].Path == parentPathString || map[key].InitParent == parentPathString) { - const localPathParsed = path.parse(localPath) - const localPathName = localPathParsed.name.toLowerCase() - const localPathExt = localPathParsed.ext.toLowerCase() - - // Remap adjacent matching file - if (localPathName != 'init.meta' && localPathName.endsWith('.meta') && (localPathExt == '.json' || localPathExt == '.yaml' || localPathExt == '.toml')) { - const title = localPathParsed.name.slice(0, -5) - if (fs.existsSync(localPathParsed.dir + '/' + title + '.lua')) { - delete map[key] - await mapDirectory(localPath, title + '.lua') - } else if (fs.existsSync(localPathParsed.dir + '/' + title + '.luau')) { + + } else if ((event == 'add' | event == 'addDir') && localPathStats) { + + // Added + if (parentPathString in mTimes && (!localPathStats.isFile() || localPathExtensionIsMappable(localPath))) { + console.log('A', cyan(localPath)) + for (const key in map) { + if (map[key].Path == parentPathString || map[key].InitParent == parentPathString) { + const localPathParsed = path.parse(localPath) + const localPathName = localPathParsed.name.toLowerCase() + const localPathExt = localPathParsed.ext.toLowerCase() + + // Remap adjacent matching file + if (localPathName != 'init.meta' && localPathName.endsWith('.meta') && (localPathExt == '.json' || localPathExt == '.yaml' || localPathExt == '.toml')) { + const title = localPathParsed.name.slice(0, -5) + if (fs.existsSync(localPathParsed.dir + '/' + title + '.lua')) { + delete map[key] + await mapDirectory(localPath, title + '.lua') + } else if (fs.existsSync(localPathParsed.dir + '/' + title + '.luau')) { + delete map[key] + await mapDirectory(localPath, title + '.luau') + } else { + console.error(fileError(localPath), yellow('Stray meta file')) + return + } + + // Remap parent folder + } else if (localPathIsInit(localPath) || localPathName == 'init.meta' && (localPathExt == '.json' || localPathExt == '.yaml' || localPathExt == '.toml') || localPathParsed.base == 'default.project.json') { delete map[key] - await mapDirectory(localPath, title + '.luau') + await mapDirectory(parentPathString, key) + + // Map only file or directory } else { - console.error(fileError(localPath), yellow('Stray meta file')) - return + await mapDirectory(localPath, key + '/' + localPathParsed.base) } - - // Remap parent folder - } else if (localPathIsInit(localPath) || localPathName == 'init.meta' && (localPathExt == '.json' || localPathExt == '.yaml' || localPathExt == '.toml') || localPathParsed.base == 'default.project.json') { - delete map[key] - await mapDirectory(parentPathString, key) - - // Map only file or directory - } else { - await mapDirectory(localPath, key + '/' + localPathParsed.base) } } + if (!mTimes[localPath]) console.error(red('Lync bug:'), yellow('Failed to add'), cyan(localPath)) } - if (!mTimes[localPath]) console.error(red('Lync bug:'), yellow('Failed to add'), cyan(localPath)) } - } - - // Modify sourcemap - if (CONFIG.GenerateSourcemap && Object.keys(modified_sourcemap).length > 0) { - generateSourcemap(PROJECT_JSON, modified_sourcemap, projectJson) - modified_sourcemap = {} + + // Modify sourcemap + if (CONFIG.GenerateSourcemap && Object.keys(modified_sourcemap).length > 0) { + generateSourcemap(PROJECT_JSON, modified_sourcemap, projectJson) + modified_sourcemap = {} + } } } + } catch (err) { + console.error(red('Sync error:'), err) } - } catch (err) { - console.error(red('Sync error:'), err) } + runJobs(event, localPath, true) }) } diff --git a/Lync/output.js b/Lync/output.js index 032812a..139df34 100644 --- a/Lync/output.js +++ b/Lync/output.js @@ -45,7 +45,7 @@ module.exports.cyan = function(s, hideBrackets) { /** * @param {Object} from * @param {Object} to - * @param {string} append + * @param {string?} append * @returns {string} */ function iterRelative(from, to, append) { @@ -81,9 +81,10 @@ module.exports.fileError = function(s) { } /** + * @param {string} s * @param {Object} from * @param {Object} to - * @param {string} s + * @param {string?} key * @returns {string} */ module.exports.jsonError = function(s, from, to, key) { diff --git a/Lync/validator/project.js b/Lync/validator/project.js index 1136f10..3cddbbf 100644 --- a/Lync/validator/project.js +++ b/Lync/validator/project.js @@ -96,7 +96,7 @@ module.exports.validate = function(type, json, localPath) { console.error(fileError(localPath), yellow('Missing key'), green('name')) failed = true } else if (typeof json.name != 'string') { - console.error(fileError(localPath), green('name'), yellow('must be a string')) + console.error(jsonError(localPath, json, json, 'name'), yellow('Must be a string')) failed = true } @@ -104,7 +104,7 @@ module.exports.validate = function(type, json, localPath) { console.error(fileError(localPath), yellow('Missing key'), green('base')) failed = true } else if (typeof json.base != 'string') { - console.error(fileError(localPath), green('base'), yellow('must be a string')) + console.error(jsonError(localPath, json, json, 'base'), yellow('Must be a string')) failed = true } @@ -112,12 +112,12 @@ module.exports.validate = function(type, json, localPath) { console.error(fileError(localPath), yellow('Missing key'), green('build')) failed = true } else if (typeof json.build != 'string') { - console.error(fileError(localPath), green('build'), yellow('must be a string')) + console.error(jsonError(localPath, json, json, 'build'), yellow('Must be a string')) failed = true } else { const pathExt = path.parse(json.build).ext.toLowerCase() if (pathExt != '.rbxl' && pathExt != '.rbxlx') { - console.error(fileError(localPath), green('build'), yellow('must point to an overwritable RBXL or RBXLX file')) + console.error(jsonError(localPath, json, json, 'build'), yellow('Must point to an overwritable RBXL or RBXLX file')) failed = true } } @@ -126,29 +126,43 @@ module.exports.validate = function(type, json, localPath) { console.error(fileError(localPath), yellow('Missing key'), green('port')) failed = true } else if (typeof json.port != 'number') { - console.error(fileError(localPath), green('port'), yellow('must be a number')) + console.error(jsonError(localPath, json, json, 'port'), yellow('Must be a number')) failed = true } if (('remoteAddress' in json) && typeof json.remoteAddress != 'string') { - console.error(fileError(localPath), green('remoteAddress'), yellow('must be a string')) + console.error(jsonError(localPath, json, json, 'remoteAddress'), yellow('Must be a string')) failed = true } + if ('globIgnorePaths' in json) { + if (!(typeof json.globIgnorePaths == 'object' && Array.isArray(json.globIgnorePaths))) { + console.error(jsonError(localPath, json, json, 'globIgnorePaths'), yellow('Must be an array')) + failed = true + } else { + for (const index in json.globIgnorePaths) { + if (typeof json.globIgnorePaths[index] != 'string') { + console.error(jsonError(localPath, json, json.globIgnorePaths, key), yellow('Must be a string')) + failed = true + } + } + } + } + if ('sourcemapEnabled' in json) { if (!(typeof json.sourcemapEnabled == 'object' && !Array.isArray(json.sourcemapEnabled))) { - console.error(fileError(localPath), green('sourcemapEnabled'), yellow('must be an object')) + console.error(jsonError(localPath, json, json, 'sourcemapEnabled'), yellow('Must be an object')) failed = true } else { for (const key in json.sourcemapEnabled) { if (key != 'RBXM' && key != 'RBXMX' ) { - console.error(fileError(localPath), 'Unexpected key', green('sourcemapEnabled\\' + key)) + console.error(jsonError(localPath, json, json.sourcemapEnabled, key), 'Unexpected key') } else { const value = json.sourcemapEnabled[key] if (typeof value != 'boolean') { - console.error(fileError(localPath), green('sourcemapEnabled\\' + key), yellow('must be a boolean')) + console.error(jsonError(localPath, json, json.sourcemapEnabled, key), yellow('Must be a boolean')) failed = true } } @@ -158,59 +172,59 @@ module.exports.validate = function(type, json, localPath) { if ('sources' in json) { if (!(typeof json.sources == 'object' && Array.isArray(json.sources))) { - console.error(fileError(localPath), green('sources'), yellow('must be an array')) + console.error(jsonError(localPath, json, json, 'sources'), yellow('Must be an array')) failed = true } else { for (const index in json.sources) { const source = json.sources[index] if (!('name' in source)) { - console.error(fileError(localPath), yellow('Missing key'), green('sources.name')) + console.error(jsonError(localPath, json, source), yellow('Missing key'), green('name')) failed = true } else if (typeof source.name != 'string') { - console.error(fileError(localPath), green('sources.name'), yellow('must be a string')) + console.error(jsonError(localPath, json, source, 'name'), yellow('Must be a string')) failed = true } if (!('url' in source)) { - console.error(fileError(localPath), yellow('Missing key'), green('sources.url')) + console.error(jsonError(localPath, json, source), yellow('Missing key'), green('url')) failed = true } else if (typeof source.url != 'string') { - console.error(fileError(localPath), green('sources.url'), yellow('must be a string')) + console.error(jsonError(localPath, json, source, 'url'), yellow('Must be a string')) failed = true } if (!('type' in source)) { - console.error(fileError(localPath), yellow('Missing key'), green('sources.type')) + console.error(jsonError(localPath, json, source), yellow('Missing key'), green('type')) failed = true } else if (source.type != 'GET' && source.type != 'POST') { - console.error(fileError(localPath), green('sources.type'), yellow('must be GET or POST')) + console.error(jsonError(localPath, json, source, 'type'), yellow('Must be GET or POST')) failed = true } if (!('headers' in source)) { - console.error(fileError(localPath), yellow('Missing key'), green('sources.headers')) + console.error(jsonError(localPath, json, source), yellow('Missing key'), green('headers')) failed = true } else if (!(typeof source.headers == 'object' && !Array.isArray(source.headers))) { - console.error(fileError(localPath), green('sources.headers'), yellow('must be an object')) + console.error(jsonError(localPath, json, source, 'headers'), green('sources.headers'), yellow('must be an object')) failed = true } if ('postData' in source) { if (typeof source.postData != 'string' && !(typeof source.postData == 'object' && !Array.isArray(source.postData))) { - console.error(fileError(localPath), green('sources.postData'), yellow('must be a string or an object')) + console.error(jsonError(localPath, json, source, 'postData'), yellow('must be a string or an object')) failed = true } else if (source.type != 'POST') { - console.error(fileError(localPath), yellow('Cannot use key'), green('sources.postData'), yellow('with POST type')) + console.error(jsonError(localPath, json, source, 'postData'), yellow('Cannot use key with POST type')) failed = true } } if (!('path' in source)) { - console.error(fileError(localPath), yellow('Missing key'), green('sources.path')) + console.error(jsonError(localPath, json, source), yellow('Missing key'), green('path')) failed = true } else if (typeof source.path != 'string') { - console.error(fileError(localPath), green('sources.path'), yellow('must be a string')) + console.error(jsonError(localPath, json, source, 'path'), yellow('Must be a string')) failed = true } @@ -222,12 +236,51 @@ module.exports.validate = function(type, json, localPath) { && sourceKey != 'postData' && sourceKey != 'path' ) { - console.error(fileError(localPath), 'Unexpected key', green('sources[' + index + ']\\' + sourceKey)) + console.error(jsonError(localPath, json, source, sourceKey), 'Unexpected key') } } } } } + + if ('jobs' in json) { + if (!(typeof json.jobs == 'object' && Array.isArray(json.jobs))) { + console.error(jsonError(localPath, json, json, 'jobs'), yellow('Must be an array')) + failed = true + } else { + for (const index in json.jobs) { + const job = json.jobs[index] + + if (!('globPaths' in job)) { + console.error(jsonError(localPath, json, job), yellow('Missing key'), green('globPaths')) + failed = true + } else if (!(typeof job.globPaths == 'object' && Array.isArray(job.globPaths))) { + console.error(jsonError(localPath, json, job, 'globPaths'), yellow('Must be an array')) + failed = true + } else { + + } + + if (!('on' in job)) { + console.error(jsonError(localPath, json, job), yellow('Missing key'), green('on')) + failed = true + } else if (!(typeof job.on == 'object' && Array.isArray(job.on))) { + console.error(jsonError(localPath, json, job, 'on'), yellow('Must be an array')) + failed = true + } else { + + } + + if (!('afterSync' in job)) { + console.error(jsonError(localPath, json, job), yellow('Missing key'), green('afterSync')) + failed = true + } else if (typeof job.afterSync != 'boolean') { + console.error(jsonError(localPath, json, job, 'afterSync'), yellow('Must be a boolean')) + failed = true + } + } + } + } } const scanFailed = scan(json, json, localPath) diff --git a/README.md b/README.md index 8717316..a43c6b7 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Lync is a file sync tool for Roblox which is offered as an alternative to Rojo f |-|-|-| | Package Management | ✔ | ❌️ | | Custom File Downloads | ✔ | ❌️ | -| Automatic Shell Scripts | ➖ | ❌️ | +| Automated Jobs | ✔ | ❌️ | | Automatic Sourcemap Generation | ✔ | ❌️ | ### Misc Features diff --git a/Sample Project/default.project.json b/Sample Project/default.project.json index 06f4654..7695c20 100644 --- a/Sample Project/default.project.json +++ b/Sample Project/default.project.json @@ -8,6 +8,15 @@ "RBXMX": true }, + "jobs": [ + { + "globPaths": [ "**/*.txt" ], + "on": [ "start", "add", "addDir", "change", "unlink" ], + "afterSync": false, + "commandName": "Test" + } + ], + "tree": { "Workspace": { "$properties": {