@knighted/duel
Version:
TypeScript dual packages.
283 lines (282 loc) • 9.82 kB
JavaScript
import { parseArgs } from 'node:util';
import { resolve, join, dirname } from 'node:path';
import { stat } from 'node:fs/promises';
import { parseTsconfig } from 'get-tsconfig';
import { readPackageUp } from 'read-package-up';
import { logError, log } from './util.js';
const cliOptions = [
{
long: 'project',
short: 'p',
value: '[path]',
desc: "Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.",
},
{
long: 'pkg-dir',
short: 'k',
value: '[path]',
desc: 'Directory to start looking for package.json; defaults to --project.',
},
{
long: 'dirs',
short: 'd',
desc: 'Output both builds to directories inside of outDir. [esm, cjs].',
},
{
long: 'exports',
short: 'e',
value: '[mode]',
desc: 'Generate package.json exports. Values: wildcard | dir | name.',
},
{
long: 'exports-config',
value: '[path]',
desc: 'Provide explicit exports config file.',
},
{
long: 'exports-validate',
desc: 'Validate exports without writing.',
},
{
long: 'mode',
value: '[none|globals|full]',
desc: 'Optional shorthand for module transforms and syntax lowering.',
},
{
long: 'rewrite-policy',
value: '[safe|warn|skip]',
desc: 'Control specifier rewriting behavior.',
},
{
long: 'detect-dual-package-hazard',
short: 'H',
value: '[off|warn|error]',
desc: 'Detect mixed import/require use of dual packages.',
},
{
long: 'dual-package-hazard-allowlist',
value: '[pkg1,pkg2]',
desc: 'Comma-separated packages to ignore for dual package hazard checks.',
},
{
long: 'dual-package-hazard-scope',
value: '[file|project]',
desc: 'Scope for dual package hazard detection.',
},
{
long: 'verbose',
short: 'V',
desc: 'Enable verbose logging.',
},
{
long: 'copy-mode',
value: '[sources|full]',
desc: 'Control temp copy strategy (sources only vs full project).',
},
{
long: 'help',
short: 'h',
desc: 'Print this message.',
},
];
const printHelp = () => {
const bare = { bare: true };
const flags = cliOptions.map(opt => {
const value = opt.value ? ` ${opt.value}` : '';
const short = opt.short ? `-${opt.short}, ` : ' ';
const long = `--${opt.long}${value}`;
return { flag: `${short}${long}`, desc: opt.desc };
});
const maxFlag = Math.max(...flags.map(f => f.flag.length));
log('Usage: duel [options]\n', 'info', bare);
log('Options:', 'info', bare);
for (const { flag, desc } of flags) {
const pad = ' '.repeat(Math.max(2, maxFlag - flag.length + 2));
log(`${flag}${pad}${desc}`, 'info', bare);
}
};
const init = async (args) => {
let parsed;
try {
const { values } = parseArgs({
args,
options: {
project: {
type: 'string',
short: 'p',
default: 'tsconfig.json',
},
'pkg-dir': {
type: 'string',
short: 'k',
},
dirs: {
type: 'boolean',
short: 'd',
default: false,
},
exports: {
type: 'string',
short: 'e',
},
'exports-config': {
type: 'string',
},
'exports-validate': {
type: 'boolean',
default: false,
},
'rewrite-policy': {
type: 'string',
default: 'safe',
},
'detect-dual-package-hazard': {
type: 'string',
short: 'H',
default: 'warn',
},
'dual-package-hazard-allowlist': {
type: 'string',
},
'dual-package-hazard-scope': {
type: 'string',
default: 'file',
},
verbose: {
type: 'boolean',
short: 'V',
default: false,
},
'copy-mode': {
type: 'string',
default: 'sources',
},
mode: {
type: 'string',
},
help: {
type: 'boolean',
short: 'h',
default: false,
},
},
});
parsed = values;
}
catch (err) {
logError(err.message);
return false;
}
if (parsed.help) {
printHelp();
}
else {
const { project, 'pkg-dir': pkgDir, dirs, exports: exportsOpt, 'exports-config': exportsConfig, 'exports-validate': exportsValidate, 'rewrite-policy': rewritePolicy, 'detect-dual-package-hazard': detectDualPackageHazard, 'dual-package-hazard-allowlist': dualPackageHazardAllowlist, 'dual-package-hazard-scope': dualPackageHazardScope, verbose, mode, 'copy-mode': copyMode, } = parsed;
let configPath = resolve(project);
let stats;
let pkg;
if (mode && !['none', 'globals', 'full'].includes(mode)) {
logError('--mode expects one of: none | globals | full');
return false;
}
try {
stats = await stat(configPath);
}
catch {
logError(`Provided --project '${project}' resolves to ${configPath} which is not a file or directory.`);
return false;
}
const pkgSearchCwd = pkgDir ?? (stats.isDirectory() ? configPath : dirname(configPath));
pkg = await readPackageUp({ cwd: pkgSearchCwd });
if (!pkg) {
logError('No package.json file found.');
return false;
}
if (stats.isDirectory()) {
configPath = join(configPath, 'tsconfig.json');
try {
stats = await stat(configPath);
}
catch {
logError(`Provided --project '${project}' resolves to a directory ${dirname(configPath)} with no tsconfig.json.`);
return false;
}
}
if (stats.isFile()) {
const tsconfig = parseTsconfig(configPath);
const projectDir = dirname(configPath);
if (!tsconfig.compilerOptions?.outDir) {
log('No outDir defined in tsconfig.json. Build output will be in "dist".');
}
if (exportsOpt && !['wildcard', 'dir', 'name'].includes(exportsOpt)) {
logError('--exports expects one of: wildcard | dir | name');
return false;
}
if (!['safe', 'warn', 'skip'].includes(rewritePolicy)) {
logError('--rewrite-policy expects one of: safe | warn | skip');
return false;
}
if (!['off', 'warn', 'error'].includes(detectDualPackageHazard)) {
logError('--detect-dual-package-hazard expects one of: off | warn | error');
return false;
}
const hazardAllowlistProvided = dualPackageHazardAllowlist !== undefined;
const hazardAllowlistRaw = dualPackageHazardAllowlist ?? '';
const hasAllowlistContent = /[^,\s]/.test(hazardAllowlistRaw);
if (hazardAllowlistProvided && !hasAllowlistContent) {
logError('--dual-package-hazard-allowlist expects a comma-separated list of package names');
return false;
}
const hazardAllowlist = hasAllowlistContent
? hazardAllowlistRaw
.split(',')
.map(item => item.trim())
.filter(Boolean)
: [];
if (!['file', 'project'].includes(dualPackageHazardScope)) {
logError('--dual-package-hazard-scope expects one of: file | project');
return false;
}
if (!['sources', 'full'].includes(copyMode)) {
logError('--copy-mode expects one of: sources | full');
return false;
}
let modulesFinal = false;
let transformSyntaxFinal = false;
if (mode) {
if (mode === 'none') {
modulesFinal = false;
transformSyntaxFinal = false;
}
else if (mode === 'globals') {
modulesFinal = true;
transformSyntaxFinal = false;
}
else if (mode === 'full') {
modulesFinal = true;
transformSyntaxFinal = true;
}
}
return {
pkg,
dirs,
modules: modulesFinal,
transformSyntax: transformSyntaxFinal,
exports: exportsOpt,
exportsConfig,
exportsValidate,
rewritePolicy,
detectDualPackageHazard,
dualPackageHazardAllowlist: hazardAllowlist,
dualPackageHazardScope,
verbose,
copyMode,
tsconfig,
projectDir,
configPath,
};
}
}
return false;
};
export { init };