import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs'; import gulp from "gulp"; import rename from 'gulp-rename'; import { deleteAsync } from 'del'; import replace from 'gulp-replace'; import postcss from 'gulp-postcss'; import atImport from 'postcss-import'; import presetEnv from 'postcss-preset-env'; import cssnano from 'cssnano'; // Task which would delete the old dist directory if present gulp.task("build-clean", () => { return deleteAsync(["./dist"]); }); // Task which would transpile typescript to javascript gulp.task("typescript", (done) => { const tsconfigPath = path.resolve(path.join('.', 'tsconfig.build.json')); let proc; if(process.platform === 'win32') { proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', 'yarn', 'tsgo', '-p', `"${tsconfigPath}"`], { stdio: 'inherit' }); } else { proc = spawn('yarn', ['tsgo', '-p', `${tsconfigPath}`], { stdio: 'inherit' }); } proc.on('close', code => { if (code !== 0) { return done(new Error(`tsgo process exited with code ${code}`)); } done(); }); }); // Task which would bundle and minify the client-side JavaScript using Rollup gulp.task("bundle-minify-js", (done) => { const scriptFiles = []; const components = []; // 1. Get all directories inside src/components fs.readdirSync(path.join('src', 'components')).forEach(f => { if(fs.statSync(path.join('src', 'components', f)).isFile() && (f.endsWith('.js') || f.endsWith('.mjs'))) { console.log(`Adding root-level script: ${f}`); scriptFiles.push(path.join('src', 'components', f)); } else if(fs.statSync(path.join('src', 'components', f)).isDirectory()) { components.push(f); } }); components.forEach(component => { const componentPath = path.join('src', 'components', component); const scriptsPath = path.join(componentPath, 'scripts'); if (fs.existsSync(scriptsPath) && fs.statSync(scriptsPath).isDirectory()) { fs.readdirSync(scriptsPath).forEach(file => { if (file.endsWith('.js') || file.endsWith('.mjs')) { scriptFiles.push(path.join(scriptsPath, file)); } }); } fs.readdirSync(componentPath).forEach(file => { const componentScriptFiles = []; // Look for *.js / *.mjs files inside the root of the component folders if ((file.endsWith('.js') || file.endsWith('.mjs')) && fs.statSync(path.join(componentPath, file)).isFile()) { componentScriptFiles.push(path.join('src', 'components', component, file)); } scriptFiles.push(...componentScriptFiles); }); }); let clientEntryJSContent = ''; scriptFiles.forEach(scriptFile => { const relativePath = path.relative(path.join('src', 'components'), scriptFile).replace(/\\/g, '/'); clientEntryJSContent += `import './${relativePath}';\n`; }); //console.log(`Client Entry JS Content: ${clientEntryJSContent}`); fs.writeFileSync(path.join('src', 'components', 'client-entry.js'), clientEntryJSContent); let proc; if(process.platform === 'win32') { proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', 'yarn', 'rollup', '-c'], { stdio: 'inherit' }); } else { proc = spawn('yarn', ['rollup', '-c'], { stdio: 'inherit' }); } proc.on('close', code => { // Clean up the temporary entry file so that the src/ directory stays pristine const entryPath = path.join('src', 'components', 'client-entry.js'); if (fs.existsSync(entryPath)) { fs.rmSync(entryPath); } if (code !== 0) { return done(new Error(`Rollup process exited with code ${code}`)); } done(); }); }); // Task which would just create a copy of the current views directory in dist directory gulp.task("copy-ejs", function () { return gulp.src("./src/components/**/*.ejs") .pipe(rename((parsedPath) => { // `parsedPath.dirname` is the path relative to the glob base ("./src/components/") // Example A: "tooltip" // Example B: "tabs/templates" (or "tabs\templates" on Windows) // Normalize the slashes to forward slashes to prevent Windows path bugs const normalizedDir = parsedPath.dirname.replace(/\\/g, '/'); const pathParts = normalizedDir.split('/'); // The first folder level is always your component's name const componentName = pathParts[0]; // If the path includes a 'templates' directory, it's a complex component if (pathParts.includes('templates')) { // Keep the file inside a folder named after the component // This outputs to: dist/components/tabs/tab-content.ejs parsedPath.dirname = componentName; } else { // It's a simple component. Flatten it completely. // This outputs to: dist/components/tooltip.ejs parsedPath.dirname = ''; } })) .pipe(gulp.dest("./dist/components")); }); // Task which will copy the assets from the static CSS directory to the dist directory gulp.task("process-css", () => { // Gather all the component CSS files const styleFiles = []; fs.readdirSync(path.join('src', 'components')).forEach(component => { // Check the current item is a directory (i.e., a component folder) if (fs.statSync(path.join('src', 'components', component)).isDirectory()) { fs.readdirSync(path.join('src', 'components', component)).forEach(file => { const componentStyleFiles = []; // Include any JS files found in a "styles" subfolder of the component if(file === 'styles' && fs.statSync(path.join('src', 'components', component, file)).isDirectory()) { fs.readdirSync(path.join('src', 'components', component, file)).forEach(styleFile => { if(styleFile.endsWith('.css')) { componentStyleFiles.push(path.join('src', 'components', component, 'styles', styleFile)); } }); } // Look for *.css files inside the root of the component folders if (file.endsWith('.css')) { componentStyleFiles.push(path.join('src', 'components', component, file)); } styleFiles.push(...componentStyleFiles); }); } }); const libraryCSSAppendContent = styleFiles.map(styleFile => { const relativePath = path.relative(path.join('src', 'components'), styleFile).replace(/\\/g, '/'); return `@import "./${relativePath}";`; }).join('\n'); return gulp.src("./src/components/import.css") .pipe(replace(/\/\* Component Styles \*\//g, libraryCSSAppendContent)) .pipe( postcss([ atImport(), // Resolves @import paths (including node_modules) presetEnv({ stage: 1, // Allows use of future CSS features today features: { 'nesting-rules': true // Allows SCSS-like nesting natively } }), cssnano({ preset: [ 'default', { calc: false } ] }) ]) ) .on('error', function(error) { console.error("\n❌ CSS Error:", error.message); // Optionally print the code snippet if available if (error.source) { console.error(" File:", error.file); console.error(" Line:", error.line); } this.emit('end'); // Prevents Gulp from crashing completely }) .pipe(rename('components.css')) .pipe(gulp.dest("./dist/client")); }); // The default task which runs at start of the gulpfile.js gulp.task("default", gulp.series("build-clean", "typescript", "bundle-minify-js", "copy-ejs", "process-css"), () => { console.log("Done"); }); gulp.task('copy-package-files', () => { // After the default build tasks are done, we can copy the package.json, README.md, and LICENSE to the dist directory for npm publishing return gulp.src(["./package.lib.json", "./README.md", "./LICENSE"]) .pipe(rename((path) => { if(path.basename === "package.lib") { path.basename = "package"; } })) .pipe(gulp.dest("./dist")); }); gulp.task('package', gulp.series('default', 'copy-package-files'), () => { console.log("Package ready in dist/ directory"); });