mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 23:00:56 +01:00
410 lines
9.2 KiB
JavaScript
410 lines
9.2 KiB
JavaScript
import {deleteAsync} from 'del';
|
|
import {exec, spawn} from 'child_process';
|
|
import {globby} from 'globby';
|
|
import browserSync from 'browser-sync';
|
|
import chalk from 'chalk';
|
|
import commandLineArgs from 'command-line-args';
|
|
import copy from 'recursive-copy';
|
|
import esbuild from 'esbuild';
|
|
import fs from 'fs/promises';
|
|
import getPort, {portNumbers} from 'get-port';
|
|
import ora from 'ora';
|
|
import util from 'util';
|
|
import * as path from 'path';
|
|
import {readFileSync} from 'fs';
|
|
import {replace} from 'esbuild-plugin-replace';
|
|
|
|
const {serve, dev} = commandLineArgs([
|
|
{name: 'serve', type: Boolean},
|
|
{name: 'dev', type: Boolean}
|
|
]);
|
|
const outdir = 'doc/dist';
|
|
const cdndir = 'cdn';
|
|
const sitedir = 'doc/dist/site';
|
|
const spinner = ora({hideCursor: false}).start();
|
|
const execPromise = util.promisify(exec);
|
|
let childProcess;
|
|
let buildResults = [];
|
|
|
|
const bundleDirectories = [outdir, 'doc/etemplate2/_data'];
|
|
let packageData = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
const egwVersion = JSON.stringify(packageData.version.toString());
|
|
|
|
// Cleanup on exit
|
|
process.on('SIGINT', handleCleanup);
|
|
process.on('SIGTERM', handleCleanup);
|
|
|
|
//
|
|
// Runs 11ty and builds the docs. The returned promise resolves after the initial publish has completed. The child
|
|
// process and an array of strings containing any output are included in the resolved promise.
|
|
//
|
|
// To debug:
|
|
// > DEBUG=Eleventy* npx @11ty/eleventy
|
|
//
|
|
async function buildTheDocs(watch = false)
|
|
{
|
|
return new Promise(async (resolve, reject) =>
|
|
{
|
|
const afterSignal = '[eleventy.after]';
|
|
const args = ['@11ty/eleventy', '--quiet'];
|
|
const output = [];
|
|
|
|
if (watch)
|
|
{
|
|
args.push('--watch');
|
|
args.push('--incremental');
|
|
}
|
|
|
|
// To debug use this in terminal: DEBUG=Eleventy* npx @11ty/eleventy
|
|
const child = spawn('npx', args, {
|
|
timeout: 60000, // 60s
|
|
stdio: 'pipe',
|
|
cwd: 'doc/etemplate2',
|
|
shell: true // for Windows
|
|
});
|
|
|
|
child.stdout.on('data', data =>
|
|
{
|
|
if (data.includes(afterSignal))
|
|
{
|
|
return;
|
|
} // don't log the signal
|
|
output.push(data.toString());
|
|
});
|
|
|
|
if (watch)
|
|
{
|
|
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
|
|
// tells us when the first build completes.
|
|
child.stdout.on('data', data =>
|
|
{
|
|
if (data.includes(afterSignal))
|
|
{
|
|
resolve({child, output});
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
child.on('close', () =>
|
|
{
|
|
resolve({child, output});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//
|
|
// Builds the source with esbuild.
|
|
//
|
|
async function buildTheSource()
|
|
{
|
|
const alwaysExternal = [/*'@lit',*/ 'jquery'];
|
|
|
|
const cdnConfig = {
|
|
format: 'esm',
|
|
target: 'es2017',
|
|
entryPoints: [
|
|
//
|
|
// NOTE: Entry points must be mapped in package.json > exports, otherwise users won't be able to import them!
|
|
//
|
|
// The whole shebang
|
|
'./api/js/etemplate/etemplate2.ts',
|
|
// The auto-loader
|
|
//'./src/shoelace-autoloader.ts',
|
|
// Components
|
|
//...(await globby('./src/components/**/!(*.(style|test)).ts')),
|
|
// Translations
|
|
//...(await globby('./src/translations/**/*.ts')),
|
|
// Public utilities
|
|
//...(await globby('./src/utilities/**/!(*.(style|test)).ts')),
|
|
// Theme stylesheets
|
|
//...(await globby('./src/themes/**/!(*.test).ts')),
|
|
// React wrappers
|
|
//...(await globby('./src/react/**/*.ts'))
|
|
],
|
|
outdir: sitedir + '/assets/scripts',
|
|
chunkNames: 'chunks/[name].[hash]',
|
|
define: {
|
|
// Floating UI requires this to be set
|
|
'process.env.NODE_ENV': '"production"'
|
|
},
|
|
bundle: true,
|
|
//
|
|
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
|
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
|
//
|
|
// We never bundle React or @lit-labs/react though!
|
|
//
|
|
external: alwaysExternal,
|
|
splitting: true,
|
|
plugins: [
|
|
replace({
|
|
__EGROUPWARE_VERSION__: egwVersion
|
|
})
|
|
]
|
|
};
|
|
|
|
const npmConfig = {
|
|
...cdnConfig,
|
|
external: undefined,
|
|
minify: false,
|
|
packages: 'external',
|
|
outdir
|
|
};
|
|
|
|
if (serve)
|
|
{
|
|
// Use the context API to allow incremental dev builds
|
|
const contexts = await Promise.all([esbuild.context(cdnConfig), esbuild.context(npmConfig)]);
|
|
await Promise.all(contexts.map(context => context.rebuild()));
|
|
return contexts;
|
|
}
|
|
else
|
|
{
|
|
// Use the standard API for production builds
|
|
return await Promise.all([esbuild.build(cdnConfig), esbuild.build(npmConfig)]);
|
|
}
|
|
}
|
|
|
|
async function rollup(watch = false)
|
|
{
|
|
return new Promise(async (resolve, reject) =>
|
|
{
|
|
const afterSignal = '[rollup.after]';
|
|
const args = ['--silent'];
|
|
const output = [];
|
|
|
|
if (watch)
|
|
{
|
|
args.push('--watch');
|
|
args.push('--incremental');
|
|
}
|
|
|
|
// To debug use this in terminal: DEBUG=Eleventy* npx @11ty/eleventy
|
|
const child = spawn('rollup', args, {
|
|
stdio: 'pipe',
|
|
cwd: '.',
|
|
shell: true // for Windows
|
|
});
|
|
|
|
child.stdout.on('data', data =>
|
|
{
|
|
if (data.includes(afterSignal))
|
|
{
|
|
return;
|
|
} // don't log the signal
|
|
output.push(data.toString());
|
|
});
|
|
|
|
// Not even waiting
|
|
resolve({child, output});
|
|
});
|
|
}
|
|
|
|
//
|
|
// Called on SIGINT or SIGTERM to cleanup the build and child processes.
|
|
//
|
|
function handleCleanup()
|
|
{
|
|
buildResults.forEach(result => result.dispose());
|
|
|
|
if (childProcess)
|
|
{
|
|
childProcess.kill('SIGINT');
|
|
}
|
|
|
|
process.exit();
|
|
}
|
|
|
|
//
|
|
// Helper function to draw a spinner while tasks run.
|
|
//
|
|
async function nextTask(label, action)
|
|
{
|
|
spinner.text = label;
|
|
spinner.start();
|
|
|
|
try
|
|
{
|
|
await action();
|
|
spinner.stop();
|
|
console.log(`${chalk.green('✔')} ${label}`);
|
|
}
|
|
catch (err)
|
|
{
|
|
spinner.stop();
|
|
console.error(`${chalk.red('✘')} ${err}`);
|
|
if (err.stdout)
|
|
{
|
|
console.error(chalk.red(err.stdout));
|
|
}
|
|
if (err.stderr)
|
|
{
|
|
console.error(chalk.red(err.stderr));
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
await nextTask('Cleaning up the previous build', async () =>
|
|
{
|
|
await Promise.all([deleteAsync(sitedir), ...bundleDirectories.map(dir => deleteAsync(dir))]);
|
|
await fs.mkdir(outdir, {recursive: true});
|
|
});
|
|
|
|
await nextTask('Generating component metadata', () =>
|
|
{
|
|
return Promise.all(
|
|
bundleDirectories.map(dir =>
|
|
{
|
|
return execPromise(`node doc/scripts/metadata.mjs --outdir "${dir}"`, {stdio: 'inherit'});
|
|
})
|
|
);
|
|
});
|
|
/*
|
|
await nextTask('Generating themes', () =>
|
|
{
|
|
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, {stdio: 'inherit'});
|
|
});
|
|
*/
|
|
/* We don't do these
|
|
await nextTask('Running the TypeScript compiler', () =>
|
|
{
|
|
return execPromise(`tsc --project ./tsconfig.json --outdir "${outdir}"`, {stdio: 'inherit'});
|
|
});
|
|
await nextTask('Building source files', async () =>
|
|
{
|
|
buildResults = await buildTheSource();
|
|
});
|
|
*/
|
|
|
|
// EGroupware way of packaging
|
|
// We can't watch
|
|
await nextTask('Rolling up', async () =>
|
|
{
|
|
await rollup(dev);
|
|
});
|
|
|
|
|
|
// Launch the dev server
|
|
if (serve)
|
|
{
|
|
let result;
|
|
|
|
// Spin up Eleventy and Wait for the search index to appear before proceeding. The search index is generated during
|
|
// eleventy.after, so it appears after the docs are fully published. This is kinda hacky, but here we are.
|
|
// Kick off the Eleventy dev server with --watch and --incremental
|
|
await nextTask('Building docs', async () =>
|
|
{
|
|
result = await buildTheDocs(true);
|
|
});
|
|
|
|
const bs = browserSync.create();
|
|
const port = await getPort({port: portNumbers(4000, 4999)});
|
|
const browserSyncConfig = {
|
|
startPath: '/',
|
|
port,
|
|
logLevel: 'silent',
|
|
logPrefix: '[egw]',
|
|
logFileChanges: true,
|
|
notify: false,
|
|
single: false,
|
|
ghostMode: false,
|
|
server: {
|
|
baseDir: sitedir,
|
|
routes: {
|
|
'/dist': './cdn'
|
|
}
|
|
}
|
|
};
|
|
|
|
// Launch browser sync
|
|
bs.init(browserSyncConfig, () =>
|
|
{
|
|
const url = `http://localhost:${port}`;
|
|
console.log(chalk.cyan(`\n🥾 The dev server is available at ${url}`));
|
|
|
|
// Log deferred output
|
|
if (result.output.length > 0)
|
|
{
|
|
console.log('\n' + result.output.join('\n'));
|
|
}
|
|
|
|
// Log output that comes later on
|
|
result.child.stdout.on('data', data =>
|
|
{
|
|
console.log(data.toString());
|
|
});
|
|
});
|
|
|
|
// Rebuild and reload when source files change
|
|
bs.watch('src/**/!(*.test).*').on('change', async filename =>
|
|
{
|
|
console.log('[build] File changed: ', filename);
|
|
|
|
try
|
|
{
|
|
const isTheme = /^src\/themes/.test(filename);
|
|
const isStylesheet = /(\.css|\.styles\.ts)$/.test(filename);
|
|
|
|
// Rebuild the source
|
|
const rebuildResults = buildResults.map(result => result.rebuild());
|
|
await Promise.all(rebuildResults);
|
|
|
|
// Rebuild stylesheets when a theme file changes
|
|
if (isTheme)
|
|
{
|
|
await Promise.all(
|
|
bundleDirectories.map(dir =>
|
|
{
|
|
execPromise(`node scripts/make-themes.js --outdir "${dir}"`, {stdio: 'inherit'});
|
|
})
|
|
);
|
|
}
|
|
|
|
// Rebuild metadata (but not when styles are changed)
|
|
if (!isStylesheet)
|
|
{
|
|
await Promise.all(
|
|
bundleDirectories.map(dir =>
|
|
{
|
|
return execPromise(`node scripts/make-metadata.js --outdir "${dir}"`, {stdio: 'inherit'});
|
|
})
|
|
);
|
|
}
|
|
|
|
bs.reload();
|
|
}
|
|
catch (err)
|
|
{
|
|
console.error(chalk.red(err));
|
|
}
|
|
});
|
|
|
|
// Reload without rebuilding when the docs change
|
|
bs.watch([`${sitedir}/**/*.*`]).on('change', filename =>
|
|
{
|
|
bs.reload();
|
|
});
|
|
}
|
|
|
|
|
|
// Build for production
|
|
if (!serve)
|
|
{
|
|
let result;
|
|
|
|
await nextTask('Building the docs', async () =>
|
|
{
|
|
result = await buildTheDocs();
|
|
});
|
|
|
|
// Log deferred output
|
|
if (result.output.length > 0)
|
|
{
|
|
console.log('\n' + result.output.join('\n'));
|
|
}
|
|
process.exit(0);
|
|
}
|