const gulp = require('gulp'); const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const archiver = require('archiver'); const stringify = require('json-stringify-pretty-compact'); const typescript = require('typescript'); const ts = require('gulp-typescript'); const less = require('gulp-less'); const sass = require('gulp-sass'); const git = require('gulp-git'); const argv = require('yargs').argv; sass.compiler = require('sass'); function getConfig() { const configPath = path.resolve(process.cwd(), 'foundryconfig.json'); let config; if (fs.existsSync(configPath)) { config = fs.readJSONSync(configPath); return config; } else { return; } } function getManifest() { const json = {}; if (fs.existsSync('src')) { json.root = 'src'; } else { json.root = 'dist'; } const modulePath = path.join(json.root, 'module.json'); const systemPath = path.join(json.root, 'system.json'); if (fs.existsSync(modulePath)) { json.file = fs.readJSONSync(modulePath); json.name = 'module.json'; } else if (fs.existsSync(systemPath)) { json.file = fs.readJSONSync(systemPath); json.name = 'system.json'; } else { return; } return json; } /** * TypeScript transformers * @returns {typescript.TransformerFactory} */ function createTransformer() { /** * @param {typescript.Node} node */ function shouldMutateModuleSpecifier(node) { if ( !typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node) ) return false; if (node.moduleSpecifier === undefined) return false; if (!typescript.isStringLiteral(node.moduleSpecifier)) return false; if ( !node.moduleSpecifier.text.startsWith('./') && !node.moduleSpecifier.text.startsWith('../') ) return false; if (path.extname(node.moduleSpecifier.text) !== '') return false; return true; } /** * Transforms import/export declarations to append `.js` extension * @param {typescript.TransformationContext} context */ function importTransformer(context) { return (node) => { /** * @param {typescript.Node} node */ function visitor(node) { if (shouldMutateModuleSpecifier(node)) { if (typescript.isImportDeclaration(node)) { const newModuleSpecifier = typescript.createLiteral( `${node.moduleSpecifier.text}.js` ); return typescript.updateImportDeclaration( node, node.decorators, node.modifiers, node.importClause, newModuleSpecifier ); } else if (typescript.isExportDeclaration(node)) { const newModuleSpecifier = typescript.createLiteral( `${node.moduleSpecifier.text}.js` ); return typescript.updateExportDeclaration( node, node.decorators, node.modifiers, node.exportClause, newModuleSpecifier ); } } return typescript.visitEachChild(node, visitor, context); } return typescript.visitNode(node, visitor); }; } return importTransformer; } const tsConfig = ts.createProject('tsconfig.json', { getCustomTransformers: (_program) => ({ after: [createTransformer()], }), }); /********************/ /* BUILD */ /********************/ /** * Build TypeScript */ function buildTS() { return gulp.src('src/**/*.ts').pipe(tsConfig()).pipe(gulp.dest('dist')); } /** * Build Less */ function buildLess() { return gulp.src('src/*.less').pipe(less()).pipe(gulp.dest('dist')); } /** * Build SASS */ function buildSASS() { return gulp .src('src/*.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('dist')); } /** * Copy static files */ async function copyFiles() { const statics = [ 'lang', 'fonts', 'assets', 'templates', 'module.json', 'system.json', 'template.json', ]; try { for (const file of statics) { if (fs.existsSync(path.join('src', file))) { await fs.copy(path.join('src', file), path.join('dist', file)); } } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /** * Watch for changes for each build step */ function buildWatch() { gulp.watch('src/**/*.ts', { ignoreInitial: false }, buildTS); gulp.watch('src/**/*.less', { ignoreInitial: false }, buildLess); gulp.watch('src/**/*.scss', { ignoreInitial: false }, buildSASS); gulp.watch( ['src/fonts', 'src/lang', 'src/templates', 'src/*.json'], { ignoreInitial: false }, copyFiles ); } /********************/ /* CLEAN */ /********************/ /** * Remove built files from `dist` folder * while ignoring source files */ async function clean() { const name = path.basename(path.resolve('.')); const files = []; // If the project uses TypeScript if (fs.existsSync(path.join('src', `${name}.ts`))) { files.push( 'lang', 'templates', 'assets', 'module', `${name}.js`, 'module.json', 'system.json', 'template.json' ); } // If the project uses Less or SASS if ( fs.existsSync(path.join('src', `${name}.less`)) || fs.existsSync(path.join('src', `${name}.scss`)) ) { files.push('fonts', `${name}.css`); } console.log(' ', chalk.yellow('Files to clean:')); console.log(' ', chalk.blueBright(files.join('\n '))); // Attempt to remove the files try { for (const filePath of files) { await fs.remove(path.join('dist', filePath)); } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /********************/ /* LINK */ /********************/ /** * Link build to User Data folder */ async function linkUserData() { const name = path.basename(path.resolve('.')); const config = fs.readJSONSync('foundryconfig.json'); let destDir; try { if ( fs.existsSync(path.resolve('.', 'dist', 'module.json')) || fs.existsSync(path.resolve('.', 'src', 'module.json')) ) { destDir = 'modules'; } else if ( fs.existsSync(path.resolve('.', 'dist', 'system.json')) || fs.existsSync(path.resolve('.', 'src', 'system.json')) ) { destDir = 'systems'; } else { throw Error( `Could not find ${chalk.blueBright( 'module.json' )} or ${chalk.blueBright('system.json')}` ); } let linkDir; if (config.dataPath) { if (!fs.existsSync(path.join(config.dataPath, 'Data'))) throw Error('User Data path invalid, no Data directory found'); linkDir = path.join(config.dataPath, 'Data', destDir, name); } else { throw Error('No User Data path defined in foundryconfig.json'); } if (argv.clean || argv.c) { console.log( chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`) ); await fs.remove(linkDir); } else if (!fs.existsSync(linkDir)) { console.log( chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`) ); await fs.symlink(path.resolve('./dist'), linkDir); } return Promise.resolve(); } catch (err) { Promise.reject(err); } } /*********************/ /* PACKAGE */ /*********************/ /** * Package build */ async function packageBuild() { const manifest = getManifest(); return new Promise((resolve, reject) => { try { // Remove the package dir without doing anything else if (argv.clean || argv.c) { console.log(chalk.yellow('Removing all packaged files')); fs.removeSync('package'); return; } // Ensure there is a directory to hold all the packaged versions fs.ensureDirSync('package'); // Initialize the zip file const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`; const zipFile = fs.createWriteStream(path.join('package', zipName)); const zip = archiver('zip', { zlib: { level: 9 } }); zipFile.on('close', () => { console.log(chalk.green(zip.pointer() + ' total bytes')); console.log( chalk.green(`Zip file ${zipName} has been written`) ); return resolve(); }); zip.on('error', (err) => { throw err; }); zip.pipe(zipFile); // Add the directory with the final code zip.directory('dist/', manifest.file.name); zip.finalize(); } catch (err) { return reject(err); } }); } /*********************/ /* PACKAGE */ /*********************/ /** * Update version and URLs in the manifest JSON */ function updateManifest(cb) { const packageJson = fs.readJSONSync('package.json'); const config = getConfig(), manifest = getManifest(), rawURL = config.rawURL, repoURL = config.repository, manifestRoot = manifest.root; if (!config) cb(Error(chalk.red('foundryconfig.json not found'))); if (!manifest) cb(Error(chalk.red('Manifest JSON not found'))); if (!rawURL || !repoURL) cb( Error( chalk.red( 'Repository URLs not configured in foundryconfig.json' ) ) ); try { const version = argv.update || argv.u; /* Update version */ const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/; const currentVersion = manifest.file.version; let targetVersion = ''; if (!version) { cb(Error('Missing version number')); } if (versionMatch.test(version)) { targetVersion = version; } else { targetVersion = currentVersion.replace( versionMatch, (substring, major, minor, patch) => { console.log( substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1 ); if (version === 'major') { return `${Number(major) + 1}.0.0`; } else if (version === 'minor') { return `${major}.${Number(minor) + 1}.0`; } else if (version === 'patch') { return `${major}.${minor}.${Number(patch) + 1}`; } else { return ''; } } ); } if (targetVersion === '') { return cb(Error(chalk.red('Error: Incorrect version arguments.'))); } if (targetVersion === currentVersion) { return cb( Error( chalk.red( 'Error: Target version is identical to current version.' ) ) ); } console.log(`Updating version number to '${targetVersion}'`); packageJson.version = targetVersion; manifest.file.version = targetVersion; /* Update URLs */ const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`; manifest.file.url = repoURL; manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`; manifest.file.download = result; const prettyProjectJson = stringify(manifest.file, { maxLength: 35, indent: '\t', }); fs.writeJSONSync('package.json', packageJson, { spaces: '\t' }); fs.writeFileSync( path.join(manifest.root, manifest.name), prettyProjectJson, 'utf8' ); return cb(); } catch (err) { cb(err); } } function gitAdd() { return gulp.src('package').pipe(git.add({ args: '--no-all' })); } function gitCommit() { return gulp.src('./*').pipe( git.commit(`v${getManifest().file.version}`, { args: '-a', disableAppendPaths: true, }) ); } function gitTag() { const manifest = getManifest(); return git.tag( `v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => { if (err) throw err; } ); } const execGit = gulp.series(gitAdd, gitCommit, gitTag); const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles); exports.build = gulp.series(clean, execBuild); exports.watch = buildWatch; exports.clean = clean; exports.link = linkUserData; exports.package = packageBuild; exports.update = updateManifest; exports.publish = gulp.series( clean, updateManifest, execBuild, packageBuild, execGit );