mirror of
https://github.com/shoelace-style/shoelace.git
synced 2026-01-12 02:59:13 +00:00
Shoemaker rework
This commit is contained in:
140
scripts/build.cjs
Normal file
140
scripts/build.cjs
Normal file
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// Builds the project. To spin up a dev server, pass the --serve flag.
|
||||
//
|
||||
const bs = require('browser-sync').create();
|
||||
const chalk = require('chalk');
|
||||
const commandLineArgs = require('command-line-args');
|
||||
const copy = require('recursive-copy');
|
||||
const del = require('del');
|
||||
const esbuild = require('esbuild');
|
||||
const execSync = require('child_process').execSync;
|
||||
const getPort = require('get-port');
|
||||
const glob = require('tiny-glob');
|
||||
const inlineImportPlugin = require('esbuild-plugin-inline-import');
|
||||
const path = require('path');
|
||||
const sass = require('sass');
|
||||
const sassPlugin = require('esbuild-plugin-sass');
|
||||
const { build } = require('esbuild');
|
||||
|
||||
const options = commandLineArgs({
|
||||
name: 'serve',
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
execSync(`rm -rf ./dist`, { stdio: 'inherit' });
|
||||
execSync('node scripts/make-metadata.cjs', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-icons.cjs', { stdio: 'inherit' });
|
||||
|
||||
(async () => {
|
||||
const entryPoints = [
|
||||
// The main dist
|
||||
'./src/shoelace.ts',
|
||||
// The whole shebang
|
||||
'./src/all.shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/*.ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/*.ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/*.ts'))
|
||||
];
|
||||
|
||||
const buildResult = await esbuild
|
||||
.build({
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
entryPoints,
|
||||
outdir: './dist',
|
||||
incremental: options.serve,
|
||||
define: {
|
||||
// Popper.js expects this to be set
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
plugins: [
|
||||
// Run inline style imports through Sass
|
||||
inlineImportPlugin({
|
||||
filter: /^sass:/,
|
||||
transform: async (contents, args) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
sass.render(
|
||||
{
|
||||
data: contents,
|
||||
includePaths: [path.dirname(args.path)]
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result.css.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
// Run all other stylesheets through Sass
|
||||
sassPlugin()
|
||||
]
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the docs distribution by copying dist into docs/dist. This is what powers the website. It can't exist in dev
|
||||
// because it will conflict with browser sync's routing to the actual dist dir.
|
||||
await del('./docs/dist');
|
||||
if (!options.serve) {
|
||||
await copy('./dist', './docs/dist');
|
||||
}
|
||||
|
||||
console.log(chalk.green('The build has been generated! 📦'));
|
||||
|
||||
if (options.serve) {
|
||||
const port = await getPort({
|
||||
port: getPort.makeRange(4000, 4999)
|
||||
});
|
||||
|
||||
console.log(chalk.cyan(`\nLaunching the Shoelace dev server at http://localhost:${port}! 🥾\n`));
|
||||
|
||||
// Launch browser sync
|
||||
bs.init({
|
||||
startPath: '/',
|
||||
port,
|
||||
logLevel: 'silent',
|
||||
logPrefix: '[shoelace]',
|
||||
logFileChanges: true,
|
||||
notify: false,
|
||||
server: {
|
||||
baseDir: 'docs',
|
||||
routes: {
|
||||
'/dist': './dist'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild and reload when source files change
|
||||
bs.watch(['src/**/*']).on('change', async filename => {
|
||||
console.log(`Source file changed - ${filename}`);
|
||||
|
||||
// NOTE: we don't run TypeDoc on every change because it's quite heavy, so changes to the docs won't be included
|
||||
// until the next time the build script runs.
|
||||
buildResult
|
||||
.rebuild()
|
||||
.then(() => bs.reload())
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
});
|
||||
|
||||
// Reload without rebuilding when the docs change
|
||||
bs.watch(['docs/**/*']).on('change', filename => {
|
||||
console.log(`Docs file changed - ${filename}`);
|
||||
bs.reload();
|
||||
});
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
}
|
||||
})();
|
||||
68
scripts/make-icons.cjs
Normal file
68
scripts/make-icons.cjs
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// This script downloads and generates icons and icon metadata.
|
||||
//
|
||||
const Promise = require('bluebird');
|
||||
const promisify = require('util').promisify;
|
||||
const chalk = require('chalk');
|
||||
const copy = require('recursive-copy');
|
||||
const del = require('del');
|
||||
const download = require('download');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fm = require('front-matter');
|
||||
const fs = require('fs').promises;
|
||||
const glob = promisify(require('glob'));
|
||||
const path = require('path');
|
||||
|
||||
const baseDir = path.dirname(__dirname);
|
||||
const iconDir = './dist/assets/icons';
|
||||
let numIcons = 0;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const version = require('bootstrap-icons/package').version;
|
||||
const srcPath = `./.cache/icons/icons-${version}`;
|
||||
const url = `https://github.com/twbs/icons/archive/v${version}.zip`;
|
||||
|
||||
try {
|
||||
await fs.stat(`${srcPath}/LICENSE.md`);
|
||||
console.log(chalk.cyan('Generating icons from cache'));
|
||||
} catch {
|
||||
// Download the source from GitHub (since not everything is published to NPM)
|
||||
console.log(chalk.cyan(`Downloading and extracting Bootstrap Icons ${version} 📦`));
|
||||
await download(url, './.cache/icons', { extract: true });
|
||||
}
|
||||
|
||||
// Copy icons
|
||||
console.log(chalk.cyan(`Copying icons and license`));
|
||||
await del([iconDir]);
|
||||
await mkdirp(iconDir);
|
||||
await Promise.all([
|
||||
copy(`${srcPath}/icons`, iconDir),
|
||||
copy(`${srcPath}/LICENSE.md`, path.join(iconDir, 'LICENSE.md')),
|
||||
copy(`${srcPath}/bootstrap-icons.svg`, './docs/assets/icons/sprite.svg', { overwrite: true })
|
||||
]);
|
||||
|
||||
// Generate metadata
|
||||
console.log(chalk.cyan(`Generating icon metadata`));
|
||||
const files = await glob(`${srcPath}/docs/content/icons/**/*.md`);
|
||||
|
||||
const metadata = await Promise.map(files, async file => {
|
||||
const name = path.basename(file, path.extname(file));
|
||||
const data = fm(await fs.readFile(file, 'utf8')).attributes;
|
||||
numIcons++;
|
||||
|
||||
return {
|
||||
name,
|
||||
title: data.title,
|
||||
categories: data.categories,
|
||||
tags: data.tags
|
||||
};
|
||||
});
|
||||
|
||||
await fs.writeFile(path.join(iconDir, 'icons.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
||||
|
||||
console.log(chalk.green(`Successfully processed ${numIcons} icons ✨\n`));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
168
scripts/make-metadata.cjs
Normal file
168
scripts/make-metadata.cjs
Normal file
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// This script runs TypeDoc and uses its output to generate components.json which is used for the docs.
|
||||
//
|
||||
const chalk = require('chalk');
|
||||
const execSync = require('child_process').execSync;
|
||||
const fs = require('fs');
|
||||
const mkdirp = require('mkdirp');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const { parse } = require('comment-parser/lib');
|
||||
|
||||
function getTagName(className) {
|
||||
return className.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).replace(/^-/, '');
|
||||
}
|
||||
|
||||
function getTypeInfo(item) {
|
||||
let type = item.type.name || '';
|
||||
|
||||
if (item.type.type === 'union') {
|
||||
const types = item.type.types.map(t => {
|
||||
if (t.type === 'literal' || t.type === 'reference') {
|
||||
type = `'${item.type.types.map(t => t.value).join(`' | '`)}'`;
|
||||
}
|
||||
|
||||
if (t.type === 'intrinsic') {
|
||||
type = item.type.types.map(t => t.name).join(' | ');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
// Splits a string of tag text into a { name, description } object
|
||||
function splitText(text) {
|
||||
const shouldSplit = text.indexOf(' - ') > -1;
|
||||
let name = '';
|
||||
let description = '';
|
||||
|
||||
if (shouldSplit) {
|
||||
const split = text.split(' - ');
|
||||
name = split[0].trim();
|
||||
description = split.slice(1).join(' - ').replace(/^- /, '');
|
||||
} else {
|
||||
description = text.trim().replace(/^-\s/, '');
|
||||
}
|
||||
|
||||
return { name, description };
|
||||
}
|
||||
|
||||
// Run typedoc
|
||||
console.log(chalk.cyan('Generating type data with TypeDoc'));
|
||||
mkdirp.sync('./.cache');
|
||||
execSync(
|
||||
'typedoc --json .cache/typedoc.json --entryPoints src/shoelace.ts --exclude "**/*+(index|.spec|.e2e).ts" --excludeExternals --excludeProtected --excludeInternal'
|
||||
);
|
||||
|
||||
const data = JSON.parse(fs.readFileSync('.cache/typedoc.json', 'utf8'));
|
||||
const modules = data.children;
|
||||
const components = modules.filter(module => module.kindString === 'Class');
|
||||
const output = {
|
||||
name: package.name,
|
||||
description: package.description,
|
||||
version: package.version,
|
||||
author: package.author,
|
||||
homepage: package.homepage,
|
||||
license: package.license,
|
||||
components: []
|
||||
};
|
||||
|
||||
components.map(async component => {
|
||||
const api = {
|
||||
className: component.name,
|
||||
tag: getTagName(component.name),
|
||||
file: component.sources[0].fileName,
|
||||
since: '',
|
||||
status: '',
|
||||
props: [],
|
||||
methods: [],
|
||||
events: [],
|
||||
slots: [],
|
||||
cssCustomProperties: [],
|
||||
parts: [],
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
// Metadata
|
||||
if (component.comment) {
|
||||
const tags = component.comment.tags;
|
||||
const dependencies = tags.filter(item => item.tag === 'dependency');
|
||||
const slots = tags.filter(item => item.tag === 'slot');
|
||||
const parts = tags.filter(item => item.tag === 'part');
|
||||
const events = tags.filter(item => item.tag === 'emit');
|
||||
|
||||
api.since = tags.find(item => item.tag === 'since').text.trim();
|
||||
api.status = tags.find(item => item.tag === 'status').text.trim();
|
||||
api.dependencies = dependencies.map(tag => tag.text.trim());
|
||||
api.slots = slots.map(tag => splitText(tag.text));
|
||||
api.parts = parts.map(tag => splitText(tag.text));
|
||||
api.events = events.map(tag => splitText(tag.text));
|
||||
} else {
|
||||
console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`));
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = component.children
|
||||
.filter(child => child.kindString === 'Property' && !child.flags.isStatic)
|
||||
.filter(child => child.comment && child.comment.shortText); // only with comments
|
||||
|
||||
props.map(prop => {
|
||||
const type = getTypeInfo(prop);
|
||||
|
||||
api.props.push({
|
||||
name: prop.name,
|
||||
description: prop.comment.shortText,
|
||||
type,
|
||||
defaultValue: prop.defaultValue
|
||||
});
|
||||
});
|
||||
|
||||
// Methods
|
||||
const methods = component.children
|
||||
.filter(child => child.kindString === 'Method' && !child.flags.isStatic)
|
||||
.filter(child => child.signatures[0].comment && child.signatures[0].comment.shortText); // only with comments
|
||||
|
||||
methods.map(method => {
|
||||
const signature = method.signatures[0];
|
||||
const params = Array.isArray(signature.parameters)
|
||||
? signature.parameters.map(param => {
|
||||
const type = getTypeInfo(param);
|
||||
return {
|
||||
name: param.name,
|
||||
type,
|
||||
defaultValue: param.defaultValue
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
api.methods.push({
|
||||
name: method.name,
|
||||
description: signature.comment.shortText,
|
||||
params
|
||||
});
|
||||
});
|
||||
|
||||
// CSS custom properties
|
||||
const stylesheet = path.resolve(path.dirname(api.file), path.parse(api.file).name + '.scss');
|
||||
if (fs.existsSync(stylesheet)) {
|
||||
const styles = fs.readFileSync(stylesheet, 'utf8');
|
||||
const parsed = parse(styles);
|
||||
const tags = parsed[0] ? parsed[0].tags : [];
|
||||
const cssCustomProperties = tags
|
||||
.filter(tag => tag.tag === 'prop')
|
||||
.map(tag => api.cssCustomProperties.push({ name: tag.tag, description: tag.description }));
|
||||
}
|
||||
|
||||
output.components.push(api);
|
||||
});
|
||||
|
||||
// Generate components.json
|
||||
(async () => {
|
||||
const filename = path.join('./dist/components.json');
|
||||
const outputJson = JSON.stringify(output, null, 2);
|
||||
|
||||
await mkdirp(path.dirname(filename));
|
||||
fs.writeFileSync(filename, outputJson, 'utf8');
|
||||
console.log(chalk.green(`Successfully generated components.json 🏷\n`));
|
||||
})();
|
||||
Reference in New Issue
Block a user