oh no where did astro go

This commit is contained in:
Cory LaViska
2024-04-17 11:20:27 -04:00
parent 8e5e039af8
commit 67b2888489
1189 changed files with 23848 additions and 35527 deletions

View File

@@ -1,280 +1,327 @@
import { deleteAsync } from 'del';
import { exec, spawn } from 'child_process';
import { dirname, join, relative } from 'path';
import { distDir, docsDir, rootDir, runScript, siteDir } from './utils.js';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { globby } from 'globby';
import { mkdir, readFile } from 'fs/promises';
import { replace } from 'esbuild-plugin-replace';
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 util from 'util';
import * as path from 'path';
import { readFileSync } from 'fs';
import { replace } from 'esbuild-plugin-replace';
import { dev, build } from 'astro';
import chokidar from 'chokidar';
const { serve } = commandLineArgs([{ name: 'serve', type: Boolean }]);
const outdir = 'dist';
const cdndir = 'cdn';
const sitedir = '_site';
const execPromise = util.promisify(exec);
let buildResults = [];
const bundleDirectories = [cdndir, outdir];
let packageData = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
const shoelaceVersion = JSON.stringify(packageData.version.toString());
import ora from 'ora';
import process from 'process';
//
// 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.
// TODO - CDN dist and unbundled dist
//
async function buildTheDocs(watch = false) {
let args = ['astro', 'build'];
if (watch) {
args.pop();
args.push('dev');
const __dirname = dirname(fileURLToPath(import.meta.url));
const isDeveloping = process.argv.includes('--develop');
const iconDir = join(distDir, 'assets/icons');
const spinner = ora({ text: 'Web Awesome', color: 'cyan' }).start();
const packageData = JSON.parse(await readFile(join(rootDir, 'package.json'), 'utf-8'));
const version = JSON.stringify(packageData.version.toString());
let buildContext;
console.log(process.cwd());
/**
* Runs the full build.
*/
async function buildAll() {
const start = Date.now();
// Rebuild and reload when source files change
chokidar.watch('src/**/!(*.test).*').on('change', async filename => {
console.log('[build] File changed: ', filename);
try {
await cleanup();
await generateManifest();
await generateReactWrappers();
await generateTypes();
await generateStyles();
await generateBundle();
await generateDocs();
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 => {
return 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' });
})
);
}
const siteDistDir = path.join(process.cwd(), 'docs', 'public', 'dist');
// await deleteAsync(siteDistDir);
// We copy the CDN build because that has everything bundled. Yes this looks weird.
// But if we do "/cdn" it requires changes all the docs to do /cdn instead of /dist.
console.log(`COPYING ${cdndir} to ${siteDistDir}`);
await copy(cdndir, siteDistDir, { overwrite: true });
} catch (err) {
console.error(chalk.red(err), '\n');
}
});
const time = (Date.now() - start) / 1000 + 's';
spinner.succeed(`The build is complete ${chalk.gray(`(finished in ${time})`)}`);
} catch (err) {
spinner.fail();
console.log(chalk.red(`\n${err}`));
}
return new Promise(async (resolve, reject) => {
const child = spawn('npx', args, {
stdio: 'pipe',
cwd: 'docs',
shell: true // for Windows
});
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', data => {
console.log(data);
});
child.stderr.on('data', data => {
console.error(data);
});
child.on('error', error => reject(error));
child.on('close', () => resolve());
});
}
//
// Builds the source with esbuild.
//
async function buildTheSource() {
const alwaysExternal = ['@lit/react', 'react'];
/** Empties the dist directory. */
async function cleanup() {
spinner.start('Cleaning up dist');
const cdnConfig = {
await deleteAsync(distDir);
await mkdir(distDir, { recursive: true });
spinner.succeed();
}
/**
* Analyzes components and generates the custom elements manifest file.
*/
function generateManifest() {
spinner.start('Generating CEM');
try {
execSync('cem analyze --config "custom-elements-manifest.js"');
} catch (error) {
console.error(`\n\n${error.message}`);
}
spinner.succeed();
return Promise.resolve();
}
/**
* Generates React wrappers for all components.
*/
function generateReactWrappers() {
spinner.start('Generating React wrappers');
try {
execSync(`node scripts/make-react.js --outdir "${distDir}"`, { stdio: 'inherit' });
} catch (error) {
console.error(`\n\n${error.message}`);
}
spinner.succeed();
return Promise.resolve();
}
/**
* Copies theme stylesheets to the dist.
*/
async function generateStyles() {
spinner.start('Copying stylesheets');
await copy(join(rootDir, 'src/themes'), join(distDir, 'themes'), { overwrite: true });
spinner.succeed();
return Promise.resolve();
}
/**
* Runs TypeScript to generate types.
*/
function generateTypes() {
spinner.start('Running the TypeScript compiler');
try {
execSync(`tsc --project ./tsconfig.prod.json --outdir "${distDir}"`);
} catch (error) {
return Promise.reject(error.stdout);
}
spinner.succeed();
return Promise.resolve();
}
/**
* Runs esbuild to generate the final dist.
*/
async function generateBundle() {
spinner.start('Bundling with esbuild');
const config = {
format: 'esm',
target: 'es2017',
target: 'es2020',
entryPoints: [
//
// NOTE: Entry points must be mapped in package.json > exports, otherwise users won't be able to import them!
// IMPORTANT: Entry points MUST be mapped in package.json => exports
//
// The whole shebang
// Utilities
'./src/webawesome.ts',
// The auto-loader
'./src/autoloader.ts',
// Components
// Autoloader + utilities
'./src/webawesome.loader.ts',
// Individual 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: cdndir,
outdir: distDir,
chunkNames: 'chunks/[name].[hash]',
define: {
// Floating UI requires this to be set
'process.env.NODE_ENV': '"production"'
'process.env.NODE_ENV': '"production"' // required by Floating UI
},
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/react though!
//
external: alwaysExternal,
splitting: true,
plugins: [
replace({
__WEBAWESOME_VERSION__: shoelaceVersion
})
]
plugins: [replace({ __WEBAWESOME_VERSION__: version })]
};
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;
if (isDeveloping) {
// Incremental builds for dev
buildContext = await esbuild.context(config);
await buildContext.rebuild();
} else {
// Use the standard API for production builds
return await Promise.all([esbuild.build(cdnConfig), esbuild.build(npmConfig)]);
}
}
//
// Called on SIGINT or SIGTERM to cleanup the build and child processes.
//
function exit() {
buildResults.forEach(result => {
if (result.dispose) {
result.dispose();
}
});
process.exit(1);
}
//
// Helper function to cleanly log tasks
//
async function nextTask(label, action) {
function clearLine() {
if (process.stdout.isTTY) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
} else {
process.stdout.write('\n');
}
// One-time build for production
esbuild.build(config);
}
spinner.succeed();
}
/**
* Incrementally rebuilds the source files. Must be called only after `generateBundle()` has been called.
*/
async function regenerateBundle() {
try {
process.stdout.write(`${chalk.yellow('•')} ${label}`);
await action();
clearLine();
process.stdout.write(`${chalk.green('✔')} ${label}\n`);
} catch (err) {
clearLine();
process.stdout.write(`${chalk.red('✘')} ${label}\n\n`);
if (err.stdout) process.stdout.write(`${chalk.red(err.stdout)}\n`);
if (err.stderr) process.stderr.write(`${chalk.red(err.stderr)}\n`);
exit();
spinner.start('Re-bundling with esbuild');
await buildContext.rebuild();
} catch (error) {
spinner.fail();
console.log(chalk.red(`\n${error}`));
}
spinner.succeed();
}
await nextTask('Cleaning up the previous build', async () => {
await Promise.all([deleteAsync(sitedir), ...bundleDirectories.map(dir => deleteAsync(dir))]);
await fs.mkdir(outdir, { recursive: true });
});
/**
* Generates the documentation site.
*/
async function generateDocs() {
spinner.start('Writing the docs');
await nextTask('Generating component metadata', () => {
return Promise.all(
bundleDirectories.map(dir => {
return execPromise(`node scripts/make-metadata.js --outdir "${dir}"`, { stdio: 'inherit' });
})
);
});
// 11ty
const output = (await runScript(join(__dirname, 'docs.js'), isDeveloping ? ['--develop'] : undefined))
// Cleanup the output
.replace('[11ty]', '')
.replace(' seconds', 's')
.replace(/\(.*?\)/, '')
.toLowerCase()
.trim();
await nextTask('Wrapping components for React', () => {
return execPromise(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
});
// Copy assets
await copy(join(docsDir, 'assets'), join(siteDir, 'assets'), { overwrite: true });
await nextTask('Generating themes', () => {
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
});
spinner.succeed(`Writing the docs ${chalk.gray(`(${output}`)})`);
}
await nextTask('Running the TypeScript compiler', () => {
return execPromise(`tsc --project ./tsconfig.prod.json --outdir "${outdir}"`, { stdio: 'inherit' });
});
// Initial build
await buildAll();
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing
await nextTask(`Copying CDN files to "${cdndir}"`, async () => {
await deleteAsync(cdndir);
await copy(outdir, cdndir);
});
await nextTask('Building source files', async () => {
buildResults = await buildTheSource();
});
// Copy the CDN build to the docs (prod only; we use a virtual directory in dev)
await nextTask(`Copying the build to "${sitedir}"`, async () => {
const siteDistDir = path.join('docs', 'public', 'dist');
await deleteAsync(siteDistDir);
// We copy the CDN build because that has everything bundled. Yes this looks weird.
// But if we do "/cdn" it requires changes all the docs to do /cdn instead of /dist.
await copy(cdndir, siteDistDir);
});
if (!isDeveloping) {
console.log(); // just a newline for readability
}
// Launch the dev server
if (serve) {
// 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 () => await buildTheDocs(true));
}
if (isDeveloping) {
spinner.start('Launching the dev server');
// Build for production
if (!serve) {
await nextTask('Building the docs', async () => {
await buildTheDocs();
const bs = browserSync.create();
const port = await getPort({ port: portNumbers(4000, 4999) });
const url = `http://localhost:${port}/`;
const reload = () => {
spinner.start('Reloading browser');
bs.reload();
spinner.succeed();
};
// Launch browser sync
bs.init(
{
startPath: '/',
port,
logLevel: 'silent',
logPrefix: '[webawesome]',
logFileChanges: true,
notify: false,
single: false,
ghostMode: false,
server: {
baseDir: siteDir,
routes: {
'/dist': './dist'
}
},
callbacks: {
ready: (_err, instance) => {
// 404 errors
instance.addMiddleware('*', (req, res) => {
if (req.url.toLowerCase().endsWith('.svg')) {
// Make sure SVGs error out in dev instead of serve the 404 page
res.writeHead(404);
} else {
res.writeHead(302, { location: '/404.html' });
}
res.end();
});
}
}
},
() => {
spinner.succeed();
console.log(`\nThe dev server is running at ${chalk.cyan(url)}\n`);
}
);
// Rebuild and reload when source files change
bs.watch('src/**/!(*.test).*').on('change', async filename => {
spinner.info(`File modified ${chalk.gray(`(${relative(rootDir, filename)})`)}`);
try {
const isTestFile = filename.includes('.test.ts');
const isJsStylesheet = filename.includes('.styles.ts');
const isCssStylesheet = filename.includes('.css');
const isComponent =
filename.includes('components/') &&
filename.includes('.ts') &&
!isJsStylesheet &&
!isCssStylesheet &&
!isTestFile;
// Re-bundle when relevant files change
if (!isTestFile && !isCssStylesheet) {
await regenerateBundle();
}
// Copy stylesheets when CSS files change
if (isCssStylesheet) {
await generateStyles();
}
// Regenerate metadata when components change
if (isComponent) {
await generateManifest();
await generateDocs();
}
reload();
} catch (err) {
console.error(chalk.red(err));
}
});
// Rebuild the docs and reload when the docs change
bs.watch(`${docsDir}/**/*.*`).on('change', async filename => {
spinner.info(`File modified ${chalk.gray(`(${relative(rootDir, filename)})`)}`);
await generateDocs();
reload();
});
}
// Cleanup on exit
process.on('SIGINT', exit);
process.on('SIGTERM', exit);
process.on('uncaughtException', function (err) {
console.error(err);
exit();
});
//
// Cleanup everything when the process terminates
//
function terminate() {
if (buildContext) {
buildContext.dispose();
}
if (spinner) {
spinner.stop();
}
process.exit();
}
process.on('SIGINT', terminate);
process.on('SIGTERM', terminate);

View File

@@ -1,19 +0,0 @@
#!/bin/sh
# https://github.com/ds300/patch-package/issues/326#issuecomment-1676204753
# For each file in the format <dependency_name>+<version>.patch
for PATCH_FILE in ./patches/*+*.patch; do
# Check if file exists to avoid issues with the wildcard in case no files match
if [[ -f "$PATCH_FILE" ]]; then
# Extract dependency name
DEP_NAME=$(basename "$PATCH_FILE" | cut -d'+' -f1)
# Delete the dependency from node_modules
if [[ -d "node_modules/$DEP_NAME" ]]; then
echo "Deleting node_modules/$DEP_NAME ..."
rm -rf "node_modules/$DEP_NAME"
else
echo "$DEP_NAME not found in node_modules!"
fi
fi
done

15
scripts/docs.js Normal file
View File

@@ -0,0 +1,15 @@
import { deleteAsync } from 'del';
import { docsDir, siteDir } from './utils.js';
import { join } from 'path';
import Eleventy from '@11ty/eleventy';
const elev = new Eleventy(docsDir, siteDir, {
quietMode: true,
configPath: join(docsDir, '.eleventy.js')
});
// Cleanup
await deleteAsync(siteDir);
// Write it
await elev.write();

View File

@@ -1,10 +0,0 @@
//
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
//
import { execSync } from 'child_process';
import commandLineArgs from 'command-line-args';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
execSync(`cem analyze --litelement --outdir "${outdir}"`, { stdio: 'inherit' });

View File

@@ -23,7 +23,7 @@ for await (const component of components) {
const tagWithoutPrefix = component.tagName.replace(/^wa-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.path.replace(/\.js$/, '.component.js');
const importPath = component.path.replace(/\.js$/, '.js');
const eventImports = (component.events || [])
.map(event => `import type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
@@ -50,7 +50,6 @@ for await (const component of components) {
${eventExports}
const tagName = '${component.tagName}'
Component.define('${component.tagName}')
${jsDoc}
const reactWrapper = createComponent({

View File

@@ -1,65 +0,0 @@
//
// This script bakes and copies themes, then generates a corresponding Lit stylesheet in dist/themes
//
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import { mkdirSync } from 'fs';
import { globbySync } from 'globby';
import path from 'path';
import prettier from 'prettier';
import stripComments from 'strip-css-comments';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const files = globbySync('./src/themes/**/[!_]*.css');
const filesToEmbed = globbySync('./src/themes/**/_*.css');
const themesDir = path.join(outdir, 'themes');
const embeds = {};
mkdirSync(themesDir, { recursive: true });
// Gather an object containing the source of all files named "_filename.css" so we can embed them later
filesToEmbed.forEach(file => {
embeds[path.basename(file)] = fs.readFileSync(file, 'utf8');
});
// Loop through each theme file, copying the .css and generating a .js version for Lit users
files.forEach(async file => {
let source = fs.readFileSync(file, 'utf8');
// If the source has "/* _filename.css */" in it, replace it with the embedded styles
Object.keys(embeds).forEach(key => {
source = source.replace(`/* ${key} */`, embeds[key]);
});
const css = await prettier.format(stripComments(source), {
parser: 'css'
});
let js = await prettier.format(
`
import { css } from 'lit';
export default css\`
${css}
\`;
`,
{ parser: 'babel-ts' }
);
let dTs = await prettier.format(
`
declare const _default: import("lit").CSSResult;
export default _default;
`,
{ parser: 'babel-ts' }
);
const cssFile = path.join(themesDir, path.basename(file));
const jsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.js'));
const dTsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.d.ts'));
fs.writeFileSync(cssFile, css, 'utf8');
fs.writeFileSync(jsFile, js, 'utf8');
fs.writeFileSync(dTsFile, dTs, 'utf8');
});

View File

@@ -33,11 +33,6 @@ export default function (plop) {
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.ts',
templateFile: 'templates/component/define.hbs'
},
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.component.ts',
templateFile: 'templates/component/component.hbs'
},
{

View File

@@ -40,3 +40,9 @@ export default class {{ properCase tag }} extends WebAwesomeElement {
return html` <slot></slot> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'{{ tag }}': {{ properCase tag }};
}
}

View File

@@ -1,12 +0,0 @@
import {{ properCase tag }} from './{{ tagWithoutPrefix tag }}.component.js';
export * from './{{ tagWithoutPrefix tag }}.component.js';
export default {{ properCase tag }};
{{ properCase tag }}.define('{{ tag }}');
declare global {
interface HTMLElementTagNameMap {
'{{ tag }}': {{ properCase tag }};
}
}

View File

@@ -5,7 +5,7 @@ meta:
layout: component
---
```html:preview
```html {.example}
<{{ tag }}></{{ tag }}>
```

57
scripts/utils.js Normal file
View File

@@ -0,0 +1,57 @@
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import childProcess from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Helpful directories
export const rootDir = dirname(__dirname);
export const distDir = join(rootDir, 'dist');
export const docsDir = join(rootDir, 'docs');
export const siteDir = join(rootDir, '_site');
/**
* Runs a script and returns a promise that resolves with the content of stdout when the script exits or rejects with
* the content of stderr when the script exits with an error.
*/
export function runScript(scriptPath, args = []) {
return new Promise((resolve, reject) => {
const child = childProcess.fork(scriptPath, args, { silent: true });
let wasInvoked = false;
let stderr = '';
let stdout = '';
child.on('error', err => {
if (wasInvoked) {
return;
}
wasInvoked = true;
reject(err);
});
// Capture output
child.stderr.on('data', data => (stderr += data));
child.stdout.on('data', data => (stdout += data));
// execute the callback once the process has finished running
child.on('exit', code => {
if (wasInvoked) {
return;
}
wasInvoked = true;
if (code === 0) {
// The process exited normally
resolve(stdout.trim());
} else {
// An error code was received
reject(new Error(stderr));
}
child.unref();
});
});
}