shoelace/scripts/build.js

287 wiersze
8.1 KiB
JavaScript
Czysty Zwykły widok Historia

2023-06-07 17:28:22 +00:00
import { deleteAsync } from 'del';
2023-06-07 20:38:13 +00:00
import { exec, spawn } from 'child_process';
2023-06-06 21:02:15 +00:00
import { globby } from 'globby';
import browserSync from 'browser-sync';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
2023-06-06 21:02:15 +00:00
import copy from 'recursive-copy';
2021-06-17 21:38:48 +00:00
import esbuild from 'esbuild';
2023-06-07 17:28:22 +00:00
import fs from 'fs/promises';
2023-06-06 21:02:15 +00:00
import getPort, { portNumbers } from 'get-port';
2023-06-07 17:28:22 +00:00
import ora from 'ora';
import util from 'util';
2021-02-26 14:09:13 +00:00
2023-06-07 17:28:22 +00:00
const { bundle, copydir, dir, serve, types } = commandLineArgs([
{ name: 'bundle', type: Boolean },
{ name: 'copydir', type: String },
{ name: 'serve', type: Boolean },
{ name: 'types', type: Boolean }
]);
2023-06-07 20:56:46 +00:00
const outdir = 'dist';
const sitedir = '_site';
const spinner = ora({ hideCursor: false }).start();
2023-06-08 19:05:39 +00:00
const execPromise = util.promisify(exec);
2023-06-07 20:56:46 +00:00
let childProcess;
let buildResult;
2023-06-06 19:46:50 +00:00
2023-06-07 20:16:00 +00:00
//
// 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.
//
2023-06-07 17:28:22 +00:00
async function buildTheDocs(watch = false) {
2023-06-08 18:46:12 +00:00
return new Promise(async (resolve, reject) => {
const afterSignal = '[eleventy.after]';
2023-06-07 20:16:00 +00:00
const args = ['@11ty/eleventy', '--quiet'];
const output = [];
if (watch) {
args.push('--watch');
args.push('--incremental');
}
const child = spawn('npx', args, {
stdio: 'pipe',
cwd: 'docs',
shell: true // for Windows
});
child.stdout.on('data', data => {
2023-06-08 18:46:12 +00:00
if (data.includes(afterSignal)) return; // don't log the signal
2023-06-07 20:16:00 +00:00
output.push(data.toString());
});
2023-06-08 18:46:12 +00:00
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 });
});
}
2023-06-06 21:02:15 +00:00
});
}
2023-06-07 20:16:00 +00:00
//
// Builds the source with esbuild.
//
2023-06-07 17:28:22 +00:00
async function buildTheSource() {
const alwaysExternal = ['@lit-labs/react', 'react'];
2023-06-07 20:16:00 +00:00
2023-06-07 17:28:22 +00:00
return await esbuild.build({
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
'./src/shoelace.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,
chunkNames: 'chunks/[name].[hash]',
incremental: serve,
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: bundle
? alwaysExternal
: [...alwaysExternal, '@floating-ui/dom', '@shoelace-style/animations', 'lit', 'qr-creator'],
splitting: true,
plugins: []
});
}
2021-05-11 12:35:31 +00:00
2023-06-07 20:16:00 +00:00
//
// Called on SIGINT or SIGTERM to cleanup the build and child processes.
//
2023-06-07 17:28:22 +00:00
function handleCleanup() {
buildResult.rebuild.dispose();
2023-06-07 18:12:34 +00:00
if (childProcess) {
childProcess.kill('SIGINT');
}
process.exit();
2023-06-07 17:28:22 +00:00
}
2023-06-07 20:16:00 +00:00
//
// Helper function to draw a spinner while tasks run.
//
2023-06-07 17:28:22 +00:00
async function nextTask(label, action) {
spinner.text = label;
spinner.start();
2021-02-26 14:09:13 +00:00
try {
2023-06-07 17:28:22 +00:00
await action();
spinner.stop();
console.log(`${chalk.green('✔')} ${label}`);
} catch (err) {
2023-06-07 17:28:22 +00:00
spinner.stop();
console.error(`${chalk.red('✘')} ${err}`);
2023-06-08 19:05:39 +00:00
if (err.stdout) console.error(chalk.red(err.stdout));
if (err.stderr) console.error(chalk.red(err.stderr));
process.exit(1);
}
2023-06-07 17:28:22 +00:00
}
2023-06-07 17:28:22 +00:00
await nextTask('Cleaning up the previous build', async () => {
2023-06-07 20:38:13 +00:00
await Promise.all([deleteAsync(outdir), deleteAsync(sitedir)]);
2023-06-07 17:28:22 +00:00
await fs.mkdir(outdir, { recursive: true });
});
2021-02-26 14:09:13 +00:00
2023-06-07 17:28:22 +00:00
await nextTask('Generating component metadata', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
await nextTask('Wrapping components for React', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
await nextTask('Generating Web Types', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`node scripts/make-web-types.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
2021-10-22 14:51:17 +00:00
2023-06-07 17:28:22 +00:00
await nextTask('Generating themes', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
await nextTask('Packaging up icons', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
await nextTask('Running the TypeScript compiler', () => {
2023-06-07 20:38:13 +00:00
return execPromise(`tsc --project ./tsconfig.prod.json --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
});
await nextTask('Building source files', async () => {
2023-06-07 20:16:00 +00:00
buildResult = await buildTheSource();
2023-06-07 17:28:22 +00:00
});
if (copydir) {
2023-06-07 20:16:00 +00:00
// Copy the build output to an additional directory
2023-06-07 17:28:22 +00:00
await nextTask(`Copying the build to "${copydir}"`, async () => {
await deleteAsync(copydir);
await copy(outdir, copydir);
});
}
2023-06-08 19:12:23 +00:00
// Launch the dev server
2023-06-07 17:28:22 +00:00
if (serve) {
2023-06-07 20:16:00 +00:00
let result;
2023-06-07 17:28:22 +00:00
// 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 () => {
2023-06-07 20:16:00 +00:00
result = await buildTheDocs(true);
2023-06-07 17:28:22 +00:00
});
const bs = browserSync.create();
const port = await getPort({ port: portNumbers(4000, 4999) });
const browserSyncConfig = {
startPath: '/',
port,
logLevel: 'silent',
logPrefix: '[shoelace]',
logFileChanges: true,
notify: false,
single: false,
ghostMode: false,
server: {
2023-06-07 20:38:13 +00:00
baseDir: sitedir,
2023-06-07 17:28:22 +00:00
routes: {
'/dist': './dist'
}
}
};
// Launch browser sync
bs.init(browserSyncConfig, () => {
const url = `http://localhost:${port}`;
2023-06-07 20:16:00 +00:00
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());
});
2023-06-07 17:28:22 +00:00
});
2021-10-08 14:11:12 +00:00
2023-06-07 17:28:22 +00:00
// Rebuild and reload when source files change
bs.watch(['src/**/!(*.test).*']).on('change', async filename => {
try {
2023-06-07 20:16:00 +00:00
const isTheme = /^src\/themes/.test(filename);
const isStylesheet = /(\.css|\.styles\.ts)$/.test(filename);
// Rebuild the source
2023-06-07 17:28:22 +00:00
await buildResult.rebuild();
// Rebuild stylesheets when a theme file changes
2023-06-07 20:16:00 +00:00
if (isTheme) {
2023-06-07 20:38:13 +00:00
await execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
}
2023-06-07 20:16:00 +00:00
// Rebuild metadata (but not when styles are changed)
if (!isStylesheet) {
2023-06-07 20:38:13 +00:00
await execPromise(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
2023-06-07 17:28:22 +00:00
}
bs.reload();
} catch (err) {
console.error(chalk.red(err));
}
2023-06-06 19:46:50 +00:00
});
2023-06-07 17:28:22 +00:00
// Reload without rebuilding when the docs change
2023-06-07 20:38:13 +00:00
bs.watch([`${sitedir}/**/*.*`]).on('change', filename => {
2023-06-07 17:28:22 +00:00
bs.reload();
});
}
2023-06-08 19:12:23 +00:00
// Build for production
2023-06-07 17:28:22 +00:00
if (!serve) {
2023-06-07 20:16:00 +00:00
let result;
await nextTask('Building the docs', async () => {
result = await buildTheDocs();
2023-06-07 17:28:22 +00:00
});
2023-06-07 20:16:00 +00:00
// Log deferred output
if (result.output.length > 0) {
console.log('\n' + result.output.join('\n'));
}
2023-06-07 17:28:22 +00:00
}
// Cleanup on exit
process.on('SIGINT', handleCleanup);
process.on('SIGTERM', handleCleanup);