refactor: update build environment
refactored to stick to my UNIX principles, in addition to exposing a cleaner GNU Make interface
This commit is contained in:
parent
219f877888
commit
fcec0ee5b1
5 changed files with 1061 additions and 3520 deletions
273
scripts/npm-pack.ts
Normal file
273
scripts/npm-pack.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import * as child_process from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import * as util from 'node:util';
|
||||
|
||||
const SCRIPTNAME = path.basename(__filename);
|
||||
|
||||
const DEFAULT_INPUT_DIR: string = path.join('build', 'release');
|
||||
const DEFAULT_OUTPUT_DIR: string = 'dist';
|
||||
const DEFAULT_ASSETS_INDEX_BASENAME: string = 'assets.txt';
|
||||
const DEFAULT_DOCS_DIRNAME: string = 'docs/';
|
||||
const DEFAULT_LIB_DIRNAME: string = 'lib/';
|
||||
|
||||
function usage(exec: string): string {
|
||||
return `
|
||||
Usage: ${exec} [OPTIONS] [INPUT] [OUTPUT] [DOCS]
|
||||
|
||||
Create a tarball from a package
|
||||
|
||||
This script is a wrapper around \`npm pack\`.
|
||||
|
||||
This project uses npm pack to distribute its assets, even though it's not the
|
||||
most ideal as the resulting tarball has a directory structure specific to npm
|
||||
packages. The reason it is still utilized though is because it does not require
|
||||
any additional dependencies outside the Node.js/npm runtime environment.
|
||||
|
||||
It copies the current working directory to a temporary directory, moves the
|
||||
build artifacts to the root of it and then starts \`npm pack\` from within that
|
||||
directory.
|
||||
|
||||
This is necessary, because traditional directory layout of build environments
|
||||
isn't compatible with npm pack routine. With npm packages, usually build
|
||||
artifacts are transpiled into the same directory as their sources and then
|
||||
excluded via e.g. \`.gitignore\`. This is how the \`npm pack\` routine expects
|
||||
it, which gives poor isolation between sources and build artifacts.
|
||||
|
||||
Positional arguments:
|
||||
|
||||
INPUT - directory of build output used as the input for packaging
|
||||
[default:${DEFAULT_INPUT_DIR}]
|
||||
|
||||
OUTPUT - directory to output the package (tarball) into
|
||||
[default:${DEFAULT_OUTPUT_DIR}]
|
||||
|
||||
DOCS - directory containing documentation, which is used as an auxiliary
|
||||
input for packaging alongside INPUT.
|
||||
|
||||
Options:
|
||||
|
||||
-d, --docs-dirname - name of directory to output documentation into
|
||||
(if supplied through DOCS).
|
||||
[default:${DEFAULT_DOCS_DIRNAME}]
|
||||
|
||||
-l, --lib-dirname - name of directory to output transpiled module into
|
||||
[default:${DEFAULT_LIB_DIRNAME}]
|
||||
`;
|
||||
}
|
||||
|
||||
export interface PackageOptions {
|
||||
inputDir: string,
|
||||
outputDir: string,
|
||||
docsInputDir: string | null,
|
||||
docsOutputDirname: string,
|
||||
libOutputDirname: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* recursively traverse a directory and yield all paths relative to the root
|
||||
*
|
||||
* @param root - path where to start traversal from
|
||||
*/
|
||||
export function* listFiles(
|
||||
root: string,
|
||||
trueRoot?: string,
|
||||
): Generator<string, void, unknown> {
|
||||
trueRoot = trueRoot ?? root;
|
||||
if (!fs.existsSync(root)) {
|
||||
throw new Error(`path does not exist: '${root}'`);
|
||||
}
|
||||
|
||||
var files = fs.readdirSync(root);
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
let filePath = path.join(root, files[i]);
|
||||
let stat = fs.lstatSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) { yield* listFiles(filePath, trueRoot) }
|
||||
else { yield path.relative(trueRoot, filePath); }
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: write TSDOC block comment
|
||||
*/
|
||||
function pack(options: PackageOptions): void {
|
||||
var cwd = process.cwd();
|
||||
|
||||
if (path.dirname(options.inputDir) == path.basename(options.inputDir)) {
|
||||
throw new Error('inputDir must have a nesting depth of at least 2')
|
||||
}
|
||||
|
||||
if ([path.sep, '.'].includes(options.inputDir[0])) {
|
||||
throw new Error(`inputDir must be a relative path inside of '${cwd}'`)
|
||||
}
|
||||
|
||||
console.log(`${SCRIPTNAME}: creating temporary directory...`);
|
||||
const tempDir = fs.mkdtempSync(path.join(
|
||||
os.tmpdir(),
|
||||
`${path.basename(cwd)}-`
|
||||
));
|
||||
|
||||
console.log(`${SCRIPTNAME}: copying metadata...`);
|
||||
[
|
||||
'package.json',
|
||||
'LICENSE',
|
||||
'README.md',
|
||||
].forEach((target) => {
|
||||
// TODO: sync file stats
|
||||
fs.cpSync(
|
||||
path.join(cwd, target),
|
||||
path.join(tempDir, target),
|
||||
{
|
||||
filter: (src: string, dest: string) => {
|
||||
console.log(
|
||||
`cp: ${path.relative(cwd, src)} > ${dest}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (options.docsInputDir) {
|
||||
console.log(`${SCRIPTNAME}: docs supplied, will copy...`);
|
||||
// TODO: sync file stats
|
||||
fs.cpSync(
|
||||
path.join(cwd, options.docsInputDir),
|
||||
path.join(tempDir, options.docsOutputDirname),
|
||||
{
|
||||
recursive: true,
|
||||
filter: (src: string, dest: string) => {
|
||||
console.log(
|
||||
`cp: ${path.relative(cwd, src)} > ${dest}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
else {
|
||||
console.log(`${SCRIPTNAME}: no docs supplied, will not copy...`);
|
||||
}
|
||||
|
||||
console.log(`${SCRIPTNAME}: copying build output...`);
|
||||
fs.cpSync(
|
||||
options.inputDir,
|
||||
path.join(tempDir, options.libOutputDirname),
|
||||
{
|
||||
recursive: true,
|
||||
filter: (src: string, dest: string) => {
|
||||
console.log(
|
||||
`cp: ${path.relative(cwd, src)} > ${dest}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const outputDir = path.resolve(cwd, options.outputDir);
|
||||
console.log(`mkdir: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`npm: pack --pack-destination ${outputDir}`);
|
||||
child_process.execSync(
|
||||
`npm pack --pack-destination ${outputDir}`,
|
||||
{
|
||||
cwd: tempDir,
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// minimum number of positional arguments
|
||||
const minPosargs: number = 2;
|
||||
|
||||
// default values of options arguments
|
||||
const defaultOptargs: {[key: string]: any} = {
|
||||
'docs-dirname': DEFAULT_DOCS_DIRNAME,
|
||||
'lib-dirname': DEFAULT_LIB_DIRNAME,
|
||||
};
|
||||
|
||||
// required options arguments
|
||||
const requiredOptargs: string[] = [
|
||||
'docs-dirname',
|
||||
'lib-dirname',
|
||||
];
|
||||
|
||||
// the interface of parseArgs is very simple and Typescript does not play
|
||||
// nicely with it, since it expects any reassignments to be of the same type
|
||||
// as the primitives parseArgs allows. That's why I'm doing a lot of `as
|
||||
// unknown as whatever` kung-fu down below. The node runtime doesn't care
|
||||
// anyway...
|
||||
var {values, positionals} = util.parseArgs({
|
||||
options: {
|
||||
'docs-dirname': {
|
||||
type: 'string',
|
||||
short: 'd'
|
||||
},
|
||||
'lib-dirname': {
|
||||
type: 'string',
|
||||
short: 'd'
|
||||
},
|
||||
'help': {
|
||||
type: 'boolean',
|
||||
short: 'h'
|
||||
}
|
||||
},
|
||||
allowPositionals: true
|
||||
});
|
||||
|
||||
// there's probably a prettier way as to not have to reassign this just to
|
||||
// make tsc happy, but I'm a little exhausted...
|
||||
const args: string[] = positionals;
|
||||
|
||||
if (values.help != undefined) {
|
||||
const exec = [
|
||||
'ts-node',
|
||||
path.join(path.basename(__dirname), path.basename(__filename))
|
||||
].join(' ');
|
||||
console.log(usage(exec));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
values = {...defaultOptargs, ...values};
|
||||
|
||||
var errors: boolean = false;
|
||||
|
||||
for (var requiredOptarg of requiredOptargs) {
|
||||
if (!(requiredOptarg in values)) {
|
||||
console.error(
|
||||
`error: missing options argument: --${requiredOptarg}`
|
||||
);
|
||||
errors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (positionals.length < minPosargs) {
|
||||
if (positionals.length == 1) {
|
||||
positionals.push(DEFAULT_OUTPUT_DIR);
|
||||
}
|
||||
|
||||
else if (positionals.length == 0) {
|
||||
positionals.push(DEFAULT_INPUT_DIR);
|
||||
positionals.push(DEFAULT_OUTPUT_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors != false) {
|
||||
console.log(`supply -h/--help, for more information.`)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
pack({
|
||||
inputDir: positionals[0],
|
||||
outputDir: positionals[1],
|
||||
docsInputDir: positionals[2] ?? null,
|
||||
docsOutputDirname: values['docs-dirname']!,
|
||||
libOutputDirname: values['lib-dirname']!,
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue