packages/jsii-pacmak/lib/npm-modules.ts (152 lines of code) (raw):
import * as spec from '@jsii/spec';
import * as fs from 'fs-extra';
import * as path from 'path';
import { JsiiModule } from './packaging';
import { topologicalSort, Toposorted } from './toposort';
import { findDependencyDirectory, isBuiltinModule } from './util';
import * as logging from '../lib/logging';
/**
* Find all modules that need to be packagerd
*
* If the input list is empty, include the current directory.
*
* The result is topologically sorted.
*/
export async function findJsiiModules(
directories: readonly string[],
recurse: boolean,
): Promise<Toposorted<JsiiModule>> {
const ret: JsiiModule[] = [];
const visited = new Set<string>();
const toVisit = directories.length > 0 ? directories : ['.'];
await Promise.all(toVisit.map((dir) => visitPackage(dir, true)));
return topologicalSort(
ret,
(m) => m.name,
(m) => m.dependencyNames,
);
async function visitPackage(dir: string, isRoot: boolean) {
const realPath = await fs.realpath(dir);
if (visited.has(realPath)) {
return;
} // Already visited
visited.add(realPath);
const pkg = await fs.readJson(path.join(realPath, 'package.json'));
if (!pkg.jsii?.outdir || !pkg.jsii?.targets) {
if (isRoot) {
throw new Error(
`Invalid "jsii" section in ${realPath}. Expecting "outdir" and "targets"`,
);
} else {
return; // just move on, this is not a jsii package
}
}
if (!pkg.name) {
throw new Error(
`package.json does not have a 'name' field: ${JSON.stringify(
pkg,
undefined,
2,
)}`,
);
}
const dependencyNames = [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
...Object.keys(pkg.devDependencies ?? {}),
];
// if --recurse is set, find dependency dirs and build them.
if (recurse) {
await Promise.all(
dependencyNames.flatMap(async (dep) => {
if (isBuiltinModule(dep)) {
return [];
}
try {
const depDir = await findDependencyDirectory(dep, realPath);
return [await visitPackage(depDir, false)];
} catch (e: any) {
// Some modules like `@types/node` cannot be require()d, but we also don't need them.
if (
!['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(
e.code,
)
) {
throw e;
}
return [];
}
}),
);
}
// outdir is either by package.json/jsii.outdir (relative to package root) or via command line (relative to cwd)
const outputDirectory =
pkg.jsii.outdir && path.resolve(realPath, pkg.jsii.outdir);
const targets = [...Object.keys(pkg.jsii.targets), 'js']; // "js" is an implicit target.
ret.push(
new JsiiModule({
name: pkg.name,
moduleDirectory: realPath,
defaultOutputDirectory: outputDirectory,
availableTargets: targets,
dependencyNames,
}),
);
}
}
export async function updateAllNpmIgnores(
packages: JsiiModule[],
): Promise<void> {
await Promise.all(
packages.map((pkg) =>
updateNpmIgnore(pkg.moduleDirectory, pkg.outputDirectory),
),
);
}
async function updateNpmIgnore(
packageDir: string,
excludeOutdir: string | undefined,
) {
const npmIgnorePath = path.join(packageDir, '.npmignore');
let lines = new Array<string>();
let modified = false;
if (await fs.pathExists(npmIgnorePath)) {
lines = (await fs.readFile(npmIgnorePath)).toString().split('\n');
}
// if this is a fresh .npmignore, we can be a bit more opinionated
// otherwise, we add just add stuff that's critical
if (lines.length === 0) {
excludePattern(
'Exclude typescript source and config',
'*.ts',
'tsconfig.json',
'*.tsbuildinfo',
);
includePattern(
'Include javascript files and typescript declarations',
'*.js',
'*.d.ts',
);
}
if (excludeOutdir) {
excludePattern(
'Exclude jsii outdir',
path.relative(packageDir, excludeOutdir),
);
}
includePattern(
'Include .jsii and .jsii.gz',
spec.SPEC_FILE_NAME,
spec.SPEC_FILE_NAME_COMPRESSED,
);
if (modified) {
await fs.writeFile(npmIgnorePath, `${lines.join('\n')}\n`);
logging.info('Updated .npmignore');
}
function includePattern(comment: string, ...patterns: string[]) {
excludePattern(comment, ...patterns.map((p) => `!${p}`));
}
function excludePattern(comment: string, ...patterns: string[]) {
let first = true;
for (const pattern of patterns) {
if (lines.includes(pattern)) {
return; // already in .npmignore
}
modified = true;
if (first) {
lines.push('');
lines.push(`# ${comment}`);
first = false;
}
lines.push(pattern);
}
}
}