projen
Version:
CDK for software projects
1,175 lines • 198 kB
JavaScript
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstallReason = exports.NpmAccess = exports.NodePackageManager = exports.NodePackage = exports.CodeArtifactAuthProvider = void 0;
exports.defaultNpmToken = defaultNpmToken;
const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
const fs_1 = require("fs");
const path_1 = require("path");
const semver = require("semver");
const util_1 = require("./util");
const yarnrc_1 = require("./yarnrc");
const _resolve_1 = require("../_resolve");
const component_1 = require("../component");
const dependencies_1 = require("../dependencies");
const json_1 = require("../json");
const release_1 = require("../release");
const task_runtime_1 = require("../task-runtime");
const util_2 = require("../util");
const UNLICENSED = "UNLICENSED";
const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org/";
const GITHUB_PACKAGES_REGISTRY = "npm.pkg.github.com";
const DEFAULT_NPM_TOKEN_SECRET = "NPM_TOKEN";
const DEFAULT_GITHUB_TOKEN_SECRET = "GITHUB_TOKEN";
const DEFAULT_PNPM_VERSION = "10.33.0";
const DEFAULT_YARN_BERRY_VERSION = "4.13.0";
const DEFAULT_YARN_CLASSIC_VERSION = "1.22.22";
/**
* Options for authorizing requests to a AWS CodeArtifact npm repository.
*/
var CodeArtifactAuthProvider;
(function (CodeArtifactAuthProvider) {
/**
* Fixed credentials provided via Github secrets.
*/
CodeArtifactAuthProvider["ACCESS_AND_SECRET_KEY_PAIR"] = "ACCESS_AND_SECRET_KEY_PAIR";
/**
* Ephemeral credentials provided via Github's OIDC integration with an IAM role.
* See:
* https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
* https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
*/
CodeArtifactAuthProvider["GITHUB_OIDC"] = "GITHUB_OIDC";
})(CodeArtifactAuthProvider || (exports.CodeArtifactAuthProvider = CodeArtifactAuthProvider = {}));
/**
* Represents the npm `package.json` file.
*/
class NodePackage extends component_1.Component {
/**
* Returns the `NodePackage` instance associated with a project or `undefined` if
* there is no NodePackage.
* @param project The project
* @returns A NodePackage, or undefined
*/
static of(project) {
const isIt = (o) => o instanceof NodePackage;
return project.components.find(isIt);
}
/**
* The version of PNPM to use if using PNPM as a package manager.
*
* @returns `undefined` if the package manager is not PNPM.
*/
get pnpmVersion() {
return this.packageManager === NodePackageManager.PNPM
? this._pnpmVersion
: undefined;
}
/**
* The version of Bun to use if using Bun as a package manager.
*
* @returns `undefined` if the package manager is not Bun.
*/
get bunVersion() {
return this.packageManager === NodePackageManager.BUN
? this._bunVersion
: undefined;
}
/**
* The version of Yarn to use if using Yarn as a package manager.
*
* @returns `undefined` if the package manager is not Yarn.
*/
get yarnVersion() {
if ((0, util_1.isYarnBerry)(this.packageManager)) {
return this._yarnBerryVersion;
}
if ((0, util_1.isYarnClassic)(this.packageManager)) {
return DEFAULT_YARN_CLASSIC_VERSION;
}
return undefined;
}
constructor(project, options = {}) {
super(project);
this.scripts = {};
this.scriptsToBeRemoved = new Set();
this.keywords = new Set();
this.bin = {};
this.engines = {};
this.packageName = options.packageName ?? project.name;
this.peerDependencyOptions = {
pinnedDevDependency: true,
...options.peerDependencyOptions,
};
this.allowLibraryDependencies = options.allowLibraryDependencies ?? true;
// Read previous package.json early so we can use it for inference
this._prev = this.readPackageJson();
// Resolve package manager: explicit > inferred from package.json > fallback
if (options.packageManager) {
this.packageManager = options.packageManager;
this._packageManagerExplicit = true;
}
else {
this._packageManagerExplicit = false;
const fromPkgJson = inferPackageManagerFromPkgJson(this._prev, project.outdir);
if (fromPkgJson) {
this.packageManager = fromPkgJson;
}
else {
this.project.logger.warn(`[DEPRECATED] "packageManager" is not set in projenrc, defaulting to "${NodePackageManager.YARN_CLASSIC}". ` +
`This will become a required option in a future version. Please set it explicitly.`);
this.packageManager = NodePackageManager.YARN_CLASSIC;
}
}
this.entrypoint = options.entrypoint ?? "lib/index.js";
this.lockFile = determineLockfile(this.packageManager);
// Clean up lockfiles from other package managers during migration
if (options.deleteOrphanedLockFiles !== false) {
this.deleteOrphanedLockFiles();
}
this.project.annotateGenerated(`/${this.lockFile}`);
const { npmAccess, npmRegistry, npmRegistryUrl, npmTokenSecret, codeArtifactOptions, scopedPackagesOptions, npmProvenance, } = this.parseNpmOptions(options);
this.npmAccess = npmAccess;
this.npmRegistry = npmRegistry;
this.npmRegistryUrl = npmRegistryUrl;
this.npmTokenSecret = npmTokenSecret;
this.codeArtifactOptions = codeArtifactOptions;
this.scopedPackagesOptions = scopedPackagesOptions;
this.npmProvenance = npmProvenance;
this.processDeps(options);
// empty objects are here to preserve order for backwards compatibility
this.manifest = {
name: this.packageName,
description: options.description,
repository: !options.repository
? undefined
: {
type: "git",
url: options.repository,
directory: options.repositoryDirectory,
},
bin: () => this.renderBin(),
scripts: () => this.renderScripts(),
author: this.renderAuthor(options),
devDependencies: {},
peerDependencies: {},
dependencies: {},
bundledDependencies: [],
...this.renderPackageResolutions(),
keywords: () => this.renderKeywords(),
engines: () => this.renderEngines(),
devEngines: () => this.renderDevEngines(),
main: this.entrypoint !== "" ? this.entrypoint : undefined,
license: () => this.license ?? UNLICENSED,
homepage: options.homepage,
publishConfig: () => this.renderPublishConfig(),
typesVersions: this._prev?.typesVersions,
// in release CI builds we bump the version before we run "build" so we want
// to preserve the version number. otherwise, we always set it to 0.0.0
version: this.determineVersion(this._prev?.version),
bugs: options.bugsEmail || options.bugsUrl
? {
email: options.bugsEmail,
url: options.bugsUrl,
}
: undefined,
};
// Resolve package manager versions
this._options = options;
this._pnpmVersion = options.pnpmVersion ?? DEFAULT_PNPM_VERSION;
this._bunVersion = options.bunVersion ?? "latest";
this._yarnBerryVersion =
options.yarnBerryOptions?.version ?? DEFAULT_YARN_BERRY_VERSION;
// Configure Yarn Berry if using
if ((0, util_1.isYarnBerry)(this.packageManager)) {
this.configureYarnBerry(project, options);
}
// add tasks for scripts from options (if specified)
// @deprecated
for (const [cmdname, shell] of Object.entries(options.scripts ?? {})) {
project.addTask(cmdname, { exec: shell });
}
this.file = new json_1.JsonFile(this, "package.json", {
obj: this.manifest,
readonly: false, // we want "yarn add" to work and we have anti-tamper
newline: true, // all package managers prefer a newline, see https://github.com/projen/projen/issues/2076
committed: true, // needs to be committed so users can install the dependencies
});
this.addKeywords(...(options.keywords ?? []));
this.addBin(options.bin ?? {});
// automatically add all executable files under "bin"
if (options.autoDetectBin ?? true) {
this.autoDiscoverBinaries();
}
// node version
this.minNodeVersion = options.minNodeVersion;
this.maxNodeVersion = options.maxNodeVersion;
this.addNodeEngine();
this.configureCorepack();
this.addCodeArtifactLoginScript();
// license
if (options.licensed ?? true) {
this.license = options.license ?? "Apache-2.0";
}
this.installTask = project.addTask("install", {
description: "Install project dependencies and update lockfile (non-frozen)",
exec: this.installAndUpdateLockfileCommand,
});
this.installCiTask = project.addTask("install:ci", {
description: "Install project dependencies using frozen lockfile",
exec: this.installCommand,
});
}
/**
* Defines normal dependencies.
*
* @param deps Names modules to install. By default, the the dependency will
* be installed in the next `pnpm projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `pnpm
* add/update`. If you wish to specify a version range use this syntax:
* `module@^7`.
*/
addDeps(...deps) {
for (const dep of deps) {
this.project.deps.addDependency(dep, dependencies_1.DependencyType.RUNTIME);
}
}
/**
* Defines development/test dependencies.
*
* @param deps Names modules to install. By default, the the dependency will
* be installed in the next `pnpm projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `pnpm
* add/update`. If you wish to specify a version range use this syntax:
* `module@^7`.
*/
addDevDeps(...deps) {
for (const dep of deps) {
this.project.deps.addDependency(dep, dependencies_1.DependencyType.BUILD);
}
}
/**
* Defines peer dependencies.
*
* When adding peer dependencies, a devDependency will also be added on the
* pinned version of the declared peer. This will ensure that you are testing
* your code against the minimum version required from your consumers.
*
* @param deps Names modules to install. By default, the the dependency will
* be installed in the next `pnpm projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `pnpm
* add/update`. If you wish to specify a version range use this syntax:
* `module@^7`.
*/
addPeerDeps(...deps) {
if (Object.keys(deps).length && !this.allowLibraryDependencies) {
throw new Error(`cannot add peer dependencies to an APP project: ${Object.keys(deps).join(",")}`);
}
for (const dep of deps) {
this.project.deps.addDependency(dep, dependencies_1.DependencyType.PEER);
}
}
/**
* Defines bundled dependencies.
*
* Bundled dependencies will be added as normal dependencies as well as to the
* `bundledDependencies` section of your `package.json`.
*
* @param deps Names modules to install. By default, the the dependency will
* be installed in the next `pnpm projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `pnpm
* add/update`. If you wish to specify a version range use this syntax:
* `module@^7`.
*/
addBundledDeps(...deps) {
if (deps.length && !this.allowLibraryDependencies) {
throw new Error(`cannot add bundled dependencies to an APP project: ${deps.join(",")}`);
}
for (const dep of deps) {
this.project.deps.addDependency(dep, dependencies_1.DependencyType.BUNDLED);
}
}
/**
* Adds an `engines` requirement to your package.
* @param engine The engine (e.g. `node`)
* @param version The semantic version requirement (e.g. `^10`)
*/
addEngine(engine, version) {
this.engines[engine] = version;
}
/**
* Adds keywords to package.json (deduplicated)
* @param keywords The keywords to add
*/
addKeywords(...keywords) {
for (const k of keywords) {
this.keywords.add(k);
}
}
addBin(bins) {
for (const [k, v] of Object.entries(bins)) {
this.bin[k] = v;
}
}
/**
* Add a npm package.json script.
*
* @param name The script name
* @param command The command to execute
*/
setScript(name, command) {
this.scripts[name] = command;
}
/**
* Removes an npm script (always successful).
*
* @param name The name of the script.
*/
removeScript(name) {
// need to keep track in case there's a task of the same name
this.scriptsToBeRemoved.add(name);
delete this.scripts[name];
}
/**
* Indicates if a script by the given name is defined.
* @param name The name of the script
* @deprecated Use `project.tasks.tryFind(name)`
*/
hasScript(name) {
return this.project.tasks.tryFind(name) !== undefined;
}
/**
* Directly set fields in `package.json`.
* @escape
* @param name field name
* @param value field value
*/
addField(name, value) {
this.manifest[name] = value;
}
/**
* Sets the package version.
* @param version Package version.
*/
addVersion(version) {
this.manifest.version = version;
}
/**
* Defines resolutions for dependencies to change the normally resolved
* version of a dependency to something else.
*
* @param resolutions Names resolutions to be added. Specify a version or
* range with this syntax:
* `module@^7`
*/
addPackageResolutions(...resolutions) {
for (const resolution of resolutions) {
this.project.deps.addDependency(resolution, dependencies_1.DependencyType.OVERRIDE);
}
}
/**
* Returns the command to execute in order to install all dependencies (always frozen).
*/
get installCommand() {
return this.renderInstallCommand(true);
}
/**
* Renders `pnpm install` or `npm install` with lockfile update (not frozen)
*/
get installAndUpdateLockfileCommand() {
return this.renderInstallCommand(false);
}
/**
* Attempt to resolve the currently installed version for a given dependency.
*
* @remarks
* This method will first look through the current project's dependencies.
* If found and semantically valid (not '*'), that will be used.
* Otherwise, it will fall back to locating a `package.json` manifest for the dependency
* through node's internal resolution reading the version from there.
*
* @param dependencyName Dependency to resolve for.
*/
tryResolveDependencyVersion(dependencyName) {
try {
const fromDeps = this.project.deps.tryGetDependency(dependencyName);
const version = semver.coerce(fromDeps?.version, { loose: true });
if (version) {
return version.format();
}
}
catch { }
return (0, util_1.tryResolveDependencyVersion)(dependencyName, {
paths: [this.project.outdir],
});
}
// ---------------------------------------------------------------------------------------
synthesize() {
this._renderedDeps = this.renderDependencies();
super.synthesize();
}
postSynthesize() {
super.postSynthesize();
// Phase 1: install if package.json changed or node_modules is missing
const initialTrigger = this.determineInitialInstallTrigger();
if (initialTrigger) {
this.logInstallTrigger(initialTrigger);
this.installDependencies(initialTrigger);
}
// Phase 2: resolve "*" deps and install again if versions changed
const resolutions = this.resolveDepsAndWritePackageJson();
if (resolutions?.length) {
const trigger = {
reason: InstallReason.DEPS_RESOLVED,
resolutions,
};
this.logInstallTrigger(trigger);
this.installDependencies(trigger);
}
}
/**
* Checks whether dependencies need to be installed before dependency
* resolution.
*
* @returns a structured trigger describing the reason, or `undefined` if
* no install is needed.
*/
determineInitialInstallTrigger() {
const hasNodeModules = (0, fs_1.existsSync)((0, path_1.join)(this.project.outdir, "node_modules"));
if (this.file.changed) {
return {
reason: InstallReason.PACKAGE_JSON_CHANGED,
diff: this.file.diff(true),
};
}
if (!hasNodeModules) {
return { reason: InstallReason.NO_NODE_MODULES };
}
return undefined;
}
/**
* Logs user-facing information about why dependencies are being installed.
* Emits an info-level summary and debug-level details (reason, diff, resolutions).
*/
logInstallTrigger(trigger) {
this.project.logger.info("Installing dependencies... (use --debug for details)");
this.project.logger.debug(`Install reason: ${trigger.reason}`);
if (trigger.diff) {
this.project.logger.debug(`package.json diff:\n${trigger.diff.join("\n")}`);
}
if (trigger.resolutions?.length) {
this.project.logger.info(`Resolved dependency versions:\n${trigger.resolutions.map((r) => ` ${r}`).join("\n")}`);
}
}
/**
* The command prefix to use when executing binary commands for this package
* manager (e.g. `npx`, `pnpm exec`, `yarn`, `bunx`).
*/
get execCommand() {
return (0, util_1.execCommand)(this.packageManager);
}
/**
* The command which executes "projen".
* @deprecated use `project.projenCommand` instead.
*/
get projenCommand() {
return this.project.projenCommand;
}
/**
* Returns `true` if we are running within a CI build.
*/
get isAutomatedBuild() {
return (0, util_2.isTruthy)(process.env.CI);
}
determineVersion(currVersion) {
if (!this.isReleaseBuild) {
return "0.0.0";
}
return currVersion ?? "0.0.0";
}
/**
* Returns `true` if this is a CI release build.
*/
get isReleaseBuild() {
return (0, util_2.isTruthy)(process.env.RELEASE);
}
// -------------------------------------------------------------------------------------------
parseNpmOptions(options) {
let npmRegistryUrl = options.npmRegistryUrl;
if (options.npmRegistry) {
if (npmRegistryUrl) {
throw new Error('cannot use the deprecated "npmRegistry" together with "npmRegistryUrl". please use "npmRegistryUrl" instead.');
}
npmRegistryUrl = `https://${options.npmRegistry}`;
}
const npmr = new URL(npmRegistryUrl ?? DEFAULT_NPM_REGISTRY_URL);
if (!npmr || !npmr.hostname || !npmr.href) {
throw new Error(`unable to determine npm registry host from url ${npmRegistryUrl}. Is this really a URL?`);
}
const npmAccess = options.npmAccess ?? defaultNpmAccess(this.packageName);
if (!isScoped(this.packageName) && npmAccess === NpmAccess.RESTRICTED) {
throw new Error(`"npmAccess" cannot be RESTRICTED for non-scoped npm package "${this.packageName}"`);
}
const npmProvenance = options.npmProvenance ?? npmAccess === NpmAccess.PUBLIC;
if (npmProvenance && npmAccess !== NpmAccess.PUBLIC) {
throw new Error(`"npmProvenance" can only be enabled for public packages`);
}
const isAwsCodeArtifact = (0, release_1.isAwsCodeArtifactRegistry)(npmRegistryUrl);
const hasScopedPackage = options.scopedPackagesOptions &&
options.scopedPackagesOptions.length !== 0;
if (isAwsCodeArtifact) {
if (options.npmTokenSecret) {
throw new Error('"npmTokenSecret" must not be specified when publishing AWS CodeArtifact.');
}
else if (options.codeArtifactOptions?.authProvider ===
CodeArtifactAuthProvider.GITHUB_OIDC) {
if (options.codeArtifactOptions.accessKeyIdSecret ||
options.codeArtifactOptions.secretAccessKeySecret) {
throw new Error("access and secret key pair should not be provided when using GITHUB_OIDC auth provider for AWS CodeArtifact");
}
else if (!options.codeArtifactOptions.roleToAssume) {
throw new Error('"roleToAssume" property is required when using GITHUB_OIDC for AWS CodeArtifact options');
}
}
}
else {
if ((options.codeArtifactOptions?.accessKeyIdSecret ||
options.codeArtifactOptions?.secretAccessKeySecret ||
options.codeArtifactOptions?.roleToAssume) &&
!hasScopedPackage) {
throw new Error("codeArtifactOptions must only be specified when publishing AWS CodeArtifact or used in scoped packages.");
}
}
// apply defaults for AWS CodeArtifact
let codeArtifactOptions;
if (isAwsCodeArtifact || hasScopedPackage) {
const authProvider = options.codeArtifactOptions?.authProvider ??
CodeArtifactAuthProvider.ACCESS_AND_SECRET_KEY_PAIR;
const isAccessSecretKeyPairAuth = authProvider === CodeArtifactAuthProvider.ACCESS_AND_SECRET_KEY_PAIR;
codeArtifactOptions = {
authProvider,
accessKeyIdSecret: options.codeArtifactOptions?.accessKeyIdSecret ??
(isAccessSecretKeyPairAuth ? "AWS_ACCESS_KEY_ID" : undefined),
secretAccessKeySecret: options.codeArtifactOptions?.secretAccessKeySecret ??
(isAccessSecretKeyPairAuth ? "AWS_SECRET_ACCESS_KEY" : undefined),
roleToAssume: options.codeArtifactOptions?.roleToAssume,
};
}
return {
npmAccess,
npmRegistry: npmr.hostname + this.renderNpmRegistryPath(npmr.pathname),
npmRegistryUrl: npmr.href,
npmTokenSecret: options.npmTrustedPublishing
? undefined
: defaultNpmToken(options.npmTokenSecret, npmr.hostname),
codeArtifactOptions,
scopedPackagesOptions: this.parseScopedPackagesOptions(options.scopedPackagesOptions),
npmProvenance,
};
}
parseScopedPackagesOptions(scopedPackagesOptions) {
if (!scopedPackagesOptions) {
return undefined;
}
return scopedPackagesOptions.map((option) => {
if (!isScoped(option.scope)) {
throw new Error(`Scope must start with "@" in options, found ${option.scope}`);
}
if (!(0, release_1.isAwsCodeArtifactRegistry)(option.registryUrl)) {
throw new Error(`Only AWS Code artifact scoped registry is supported for now, found ${option.registryUrl}`);
}
const result = {
registryUrl: option.registryUrl,
scope: option.scope,
};
return result;
});
}
addCodeArtifactLoginScript() {
if (!this.scopedPackagesOptions ||
this.scopedPackagesOptions.length === 0) {
return;
}
this.project.addTask("ca:login", {
steps: [
{ exec: "which aws" }, // check that AWS CLI is installed
...this.scopedPackagesOptions.map((scopedPackagesOption) => {
const { registryUrl, scope } = scopedPackagesOption;
const { domain, region, accountId, registry } = (0, util_1.extractCodeArtifactDetails)(registryUrl);
// reference: https://docs.aws.amazon.com/codeartifact/latest/ug/npm-auth.html
const commands = [
`npm config set ${scope}:registry ${registryUrl}`,
`CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token --domain ${domain} --region ${region} --domain-owner ${accountId} --query authorizationToken --output text)`,
`npm config set //${registry}:_authToken=$CODEARTIFACT_AUTH_TOKEN`,
];
if (!this.minNodeVersion || semver.major(this.minNodeVersion) <= 16) {
commands.push(`npm config set //${registry}:always-auth=true`);
}
return {
exec: commands.join("; "),
};
}),
],
});
}
addNodeEngine() {
if (!this.minNodeVersion && !this.maxNodeVersion) {
return;
}
let nodeVersion = "";
if (this.minNodeVersion) {
nodeVersion += `>= ${this.minNodeVersion}`;
}
if (this.maxNodeVersion) {
nodeVersion += ` <= ${this.maxNodeVersion}`;
}
this.addEngine("node", nodeVersion);
}
renderNpmRegistryPath(path) {
if (!path || path == "/") {
return "";
}
else {
return path;
}
}
renderInstallCommand(frozen) {
switch (this.packageManager) {
case NodePackageManager.YARN:
case NodePackageManager.YARN_CLASSIC:
return [
"yarn install",
"--check-files", // ensure all modules exist (especially projen which was just removed).
...(frozen ? ["--frozen-lockfile"] : []),
].join(" ");
case NodePackageManager.YARN2:
case NodePackageManager.YARN_BERRY:
return [
"yarn install",
...(frozen ? ["--immutable"] : ["--no-immutable"]),
].join(" ");
case NodePackageManager.NPM:
return frozen ? "npm ci" : "npm install";
case NodePackageManager.PNPM:
return frozen
? "pnpm i --frozen-lockfile"
: "pnpm i --no-frozen-lockfile";
case NodePackageManager.BUN:
return ["bun install", ...(frozen ? ["--frozen-lockfile"] : [])].join(" ");
default:
throw new Error(`unexpected package manager ${this.packageManager}`);
}
}
processDeps(options) {
this.addDeps(...(options.deps ?? []));
this.addDevDeps(...(options.devDeps ?? []));
this.addPeerDeps(...(options.peerDeps ?? []));
this.addBundledDeps(...(options.bundledDeps ?? []));
}
renderDependencies() {
const devDependencies = {};
const peerDependencies = {};
const dependencies = {};
const bundledDependencies = new Array();
// synthetic dependencies: add a pinned build dependency to ensure we are
// testing against the minimum requirement of the peer.
if (this.peerDependencyOptions.pinnedDevDependency) {
for (const dep of this.project.deps.all.filter((d) => d.type === dependencies_1.DependencyType.PEER)) {
let req = dep.name;
// Skip if we already have a runtime dependency on this peer and no build dependency yet.
// If there is a build dep already, we need to override its version.
if (this.project.deps.tryGetDependency(dep.name, dependencies_1.DependencyType.RUNTIME) &&
!this.project.deps.tryGetDependency(dep.name, dependencies_1.DependencyType.BUILD)) {
continue;
}
if (dep.version) {
const ver = (0, util_1.minVersion)(dep.version);
if (!ver) {
throw new Error(`unable to determine minimum semver for peer dependency ${dep.name}@${dep.version}`);
}
req += "@" + ver;
}
this.addDevDeps(req);
}
}
for (const dep of this.project.deps.all) {
let version = dep.version ?? "*";
let name = dep.name;
if (name.startsWith("file:")) {
const localDependencyPath = name.substring(5);
const depPackageJson = (0, path_1.resolve)(this.project.outdir, localDependencyPath, "package.json");
const pkgFile = (0, fs_1.readFileSync)(depPackageJson, "utf8");
const pkg = JSON.parse(pkgFile);
version = localDependencyPath;
name = pkg.name;
}
switch (dep.type) {
case dependencies_1.DependencyType.BUNDLED:
bundledDependencies.push(name);
const depDecls = this.project.deps.all.filter((d) => d.name === name);
if (depDecls.some((d) => d.type === dependencies_1.DependencyType.PEER)) {
throw new Error(`unable to bundle "${name}": it cannot appear as a peer dependency (bundled would always take precedence over peer)`);
}
// I've observed that at least npm 10.8.2 will silently fail to bundle
// a dependency if it is [also] part of `devDependencies`. It must exist in
// `dependencies` and `dependencies` only.
if (depDecls.some((d) => d.type === dependencies_1.DependencyType.BUILD)) {
throw new Error(`unable to bundle "${name}": it cannot appear as a devDependency (only prod dependencies are bundled, and any dependency appearing as a devDependency is considered to be not a prod dependency)`);
}
// also add as a runtime dependency
dependencies[name] = version;
break;
case dependencies_1.DependencyType.PEER:
peerDependencies[name] = version;
break;
case dependencies_1.DependencyType.RUNTIME:
dependencies[name] = version;
break;
case dependencies_1.DependencyType.TEST:
case dependencies_1.DependencyType.DEVENV:
case dependencies_1.DependencyType.BUILD:
devDependencies[name] = version;
break;
}
}
// returns a lazy value to normalize during synthesis
const normalize = (obj) => () => (0, util_2.sorted)(obj);
// update the manifest we are about to save into `package.json`
this.manifest.devDependencies = normalize(devDependencies);
this.manifest.peerDependencies = normalize(peerDependencies);
this.manifest.dependencies = normalize(dependencies);
this.manifest.bundledDependencies = (0, util_2.sorted)(bundledDependencies);
// nothing further to do if package.json file does not exist
if (!this._prev) {
return { devDependencies, peerDependencies, dependencies };
}
const readDeps = (user, current = {}) => {
for (const [name, userVersion] of Object.entries(user)) {
const currentVersion = current[name];
// respect user version if it's not '*' or if current version is undefined
if (userVersion !== "*" || !currentVersion || currentVersion === "*") {
continue;
}
// memoize current version in memory so it is preserved when saving
user[name] = currentVersion;
}
// report removals
for (const name of Object.keys(current ?? {})) {
if (!user[name]) {
this.project.logger.verbose(`${name}: removed`);
}
}
};
readDeps(devDependencies, this._prev.devDependencies);
readDeps(dependencies, this._prev.dependencies);
readDeps(peerDependencies, this._prev.peerDependencies);
return { devDependencies, dependencies, peerDependencies };
}
/**
* Resolves any deps that do not have a specified version (e.g. `*`) and
* updates `package.json` if needed. Does not log or install — the caller
* is responsible for acting on the returned changes.
*
* @returns list of human-readable resolution changes, or `undefined` if
* `package.json` does not exist. An empty array means no changes were made.
*/
resolveDepsAndWritePackageJson() {
const outdir = this.project.outdir;
const rootPackageJson = (0, path_1.join)(outdir, "package.json");
const original = (0, fs_1.readFileSync)(rootPackageJson, "utf8");
const pkg = JSON.parse(original);
const changes = [];
const resolveDeps = (current, user) => {
const result = {};
current = current ?? {};
user = user ?? {};
for (const [name, currentDefinition] of Object.entries(user)) {
// find actual version from node_modules
let desiredVersion = currentDefinition;
if (currentDefinition === "*") {
// we already know we don't have the version in project `deps`,
// so skip straight to checking manifest.
const resolvedVersion = (0, util_1.tryResolveDependencyVersion)(name, {
paths: [this.project.outdir],
});
if (!resolvedVersion) {
this.project.logger.warn(`unable to resolve version for ${name} from installed modules`);
continue;
}
desiredVersion = `^${resolvedVersion}`;
}
if (currentDefinition !== desiredVersion) {
changes.push(`${name}: ${currentDefinition} => ${desiredVersion}`);
}
result[name] = desiredVersion;
}
// collect removed packages
for (const name of Object.keys(current)) {
if (!result[name]) {
changes.push(`${name}: removed`);
}
}
return result;
};
const rendered = this._renderedDeps;
if (!rendered) {
throw new Error("assertion failed");
}
const deps = resolveDeps(pkg.dependencies, rendered.dependencies);
const devDeps = resolveDeps(pkg.devDependencies, rendered.devDependencies);
const peerDeps = resolveDeps(pkg.peerDependencies, rendered.peerDependencies);
if (this.peerDependencyOptions.pinnedDevDependency) {
for (const [name, version] of Object.entries(peerDeps)) {
// Skip if we already have a runtime dependency on this peer
// or if devDependency version is already set.
// Relies on the "*" devDependency added in the presynth step
if (deps[name] || rendered.devDependencies[name] !== "*") {
continue;
}
// Take version and pin as dev dependency
const ver = (0, util_1.minVersion)(version);
if (!ver) {
throw new Error(`unable to determine minimum semver for peer dependency ${name}@${version}`);
}
devDeps[name] = ver;
}
}
pkg.dependencies = (0, util_2.sorted)(deps);
pkg.devDependencies = (0, util_2.sorted)(devDeps);
pkg.peerDependencies = (0, util_2.sorted)(peerDeps);
const updated = JSON.stringify(pkg, undefined, 2) + "\n";
if (original === updated) {
return [];
}
(0, util_2.writeFile)(rootPackageJson, updated);
return changes;
}
renderPackageResolutions() {
const render = () => {
const overridingDependencies = this.project.deps.all.filter((dep) => dep.type === dependencies_1.DependencyType.OVERRIDE);
if (!overridingDependencies.length) {
return undefined;
}
return Object.fromEntries(overridingDependencies.map(({ name, version = "*" }) => [
name,
version,
]));
};
switch (this.packageManager) {
case NodePackageManager.NPM:
case NodePackageManager.BUN:
return { overrides: render };
case NodePackageManager.PNPM:
return this.project.parent
? undefined
: { pnpm: { overrides: render } };
case NodePackageManager.YARN:
case NodePackageManager.YARN2:
case NodePackageManager.YARN_CLASSIC:
case NodePackageManager.YARN_BERRY:
default:
return { resolutions: render };
}
}
renderPublishConfig() {
// When npm provenance is enabled, we need to always render the public access
// But when npmAccess is the set to the default, we prefer to omit it
const shouldOmitAccess = !this.npmProvenance &&
this.npmAccess === defaultNpmAccess(this.packageName);
// omit values if they are the same as the npm defaults
return (0, _resolve_1.resolve)({
registry: this.npmRegistryUrl !== DEFAULT_NPM_REGISTRY_URL
? this.npmRegistryUrl
: undefined,
access: shouldOmitAccess ? undefined : this.npmAccess,
}, { omitEmpty: true });
}
renderKeywords() {
const kwds = Array.from(this.keywords);
return (0, util_2.sorted)(kwds.sort());
}
renderEngines() {
return (0, util_2.sorted)(this.engines);
}
renderDevEngines() {
const autoAdd = this._options.addPackageManagerToDevEngines !== false &&
this._packageManagerExplicit;
const base = autoAdd
? { packageManager: this.packageManagerToDevEngine() }
: {};
const merged = { ...base, ...this._options.devEngines };
return Object.keys(merged).length > 0 ? merged : undefined;
}
deleteOrphanedLockFiles() {
const ownLockFile = (0, path_1.join)(this.project.outdir, this.lockFile);
if (!(0, fs_1.existsSync)(ownLockFile)) {
return;
}
const allLockFiles = [
"yarn.lock",
"package-lock.json",
"pnpm-lock.yaml",
"bun.lockb",
];
for (const lf of allLockFiles) {
if (lf === this.lockFile) {
continue;
}
const path = (0, path_1.join)(this.project.outdir, lf);
if ((0, fs_1.existsSync)(path)) {
this.project.logger.info(`Deleting orphaned lockfile: ${lf}`);
(0, fs_1.unlinkSync)(path);
}
}
}
autoDiscoverBinaries() {
const binrel = "bin";
const bindir = (0, path_1.join)(this.project.outdir, binrel);
if ((0, fs_1.existsSync)(bindir)) {
for (const file of (0, fs_1.readdirSync)(bindir)) {
try {
(0, fs_1.accessSync)((0, path_1.join)(bindir, file), fs_1.constants.X_OK);
const binPath = (0, path_1.join)(binrel, file);
const normalizedPath = (0, util_2.normalizePersistedPath)(binPath);
this.bin[file] = normalizedPath;
}
catch (e) {
// not executable, skip
}
}
}
}
renderAuthor(options) {
let author;
if (options.authorName) {
author = {
name: options.authorName,
email: options.authorEmail,
url: options.authorUrl,
organization: options.authorOrganization ?? false,
};
}
else {
if (options.authorEmail ||
options.authorUrl ||
options.authorOrganization !== undefined) {
throw new Error('"authorName" is required if specifying "authorEmail" or "authorUrl"');
}
}
return author;
}
renderBin() {
const entries = Object.entries(this.bin);
// Yarn Berry rewrites `"bin": {"name": "./path"}` to `"bin": "./path"`
// when the key matches the package name, causing anti-tamper failures.
// Render the string form directly to avoid this.
// See: https://github.com/yarnpkg/berry/issues/6184
const { name: binName } = parsePackageName(this.packageName);
if ((0, util_1.isYarnBerry)(this.packageManager) && entries.length === 1) {
const [key, value] = entries[0];
if (key === binName) {
return value;
}
}
return (0, util_2.sorted)(this.bin);
}
renderScripts() {
const result = {};
const tasks = this.project.tasks.all
.filter((t) =>
// Must remove to prevent overriding built-in npm command (which would loop)
t.name !== this.installTask.name &&
t.name !== this.installCiTask.name)
.sort((x, y) => x.name.localeCompare(y.name));
const scriptCommand = this.resolveScriptCommand();
for (const task of tasks) {
if (this.scriptsToBeRemoved.has(task.name)) {
continue;
}
result[task.name] = `${scriptCommand} ${task.name}`;
}
return {
...result,
...this.scripts,
};
}
/**
* Determines the command to use for projen in package.json scripts.
*
* Since `node_modules/.bin` is always on PATH in npm scripts, we can use
* bare `projen` unless the user has set a custom `projenCommand`.
*/
resolveScriptCommand() {
const cmd = this.project.projenCommand;
const isDefault = [
// NodeProject sets projenCommand based on the package manager
(0, util_1.execCommand)(this.packageManager, "projen"),
// Plain Project always defaults to "npx projen" regardless of package manager
"npx projen",
].includes(cmd);
return isDefault ? "projen" : cmd;
}
readPackageJson() {
const file = (0, path_1.join)(this.project.outdir, "package.json");
if (!(0, fs_1.existsSync)(file)) {
return undefined;
}
return JSON.parse((0, fs_1.readFileSync)(file, "utf-8"));
}
/**
* Runs the install or install:ci task. Does not log — the caller is
* responsible for informing the user before calling this method.
*
* @param _trigger the reason for the install, available for subclasses to act on.
*/
installDependencies(_trigger) {
const runtime = new task_runtime_1.TaskRuntime(this.project.outdir);
const taskToRun = this.isAutomatedBuild
? this.installCiTask
: this.installTask;
runtime.runTask(taskToRun.name);
}
/**
* Sets the `packageManager` field in `package.json` for corepack-managed
* package managers (yarn berry and pnpm).
*/
configureCorepack() {
if ((0, util_1.isYarnBerry)(this.packageManager)) {
this.addField("packageManager", `yarn@${this._yarnBerryVersion}`);
}
else if (this.packageManager === NodePackageManager.PNPM) {
this.addField("packageManager", `pnpm@${this._pnpmVersion}`);
}
}
/**
* Maps the current package manager to a `DevEngineDependency`.
*/
packageManagerToDevEngine() {
const onFail = "ignore";
switch (this.packageManager) {
case NodePackageManager.YARN:
case NodePackageManager.YARN_CLASSIC:
return {
name: "yarn",
version: DEFAULT_YARN_CLASSIC_VERSION,
onFail,
};
case NodePackageManager.YARN2:
case NodePackageManager.YARN_BERRY:
return {
name: "yarn",
version: this._options.yarnBerryOptions?.version ??
`>=${semver.major(this._yarnBerryVersion)}`,
onFail,
};
case NodePackageManager.NPM:
return { name: "npm", onFail };
case NodePackageManager.PNPM:
return {
name: "pnpm",
version: this._options.pnpmVersion ?? `>=${semver.major(this._pnpmVersion)}`,
onFail,
};
case NodePackageManager.BUN:
return { name: "bun", onFail };
}
}
configureYarnBerry(project, options) {
const { yarnRcOptions = {}, zeroInstalls = false } = options.yarnBerryOptions || {};
this.checkForConflictingYarnOptions(yarnRcOptions);
this.configureYarnBerryGitignore(zeroInstalls);
new yarnrc_1.Yarnrc(project, this._yarnBerryVersion, yarnRcOptions);
}
checkForConflictingYarnOptions(yarnRcOptions) {
if (this.npmAccess &&
yarnRcOptions.npmPublishAccess &&
this.npmAccess.toString() !== yarnRcOptions.npmPublishAccess.toString()) {
throw new Error(`Cannot set npmAccess (${this.npmAccess}) and yarnRcOptions.npmPublishAccess (${yarnRcOptions.npmPublishAccess}) to different values.`);
}
if (this.npmRegistryUrl &&
yarnRcOptions.npmRegistryServer &&
this.npmRegistryUrl !== yarnRcOptions.npmRegistryServer) {
throw new Error(`Cannot set npmRegistryUrl (${this.npmRegistryUrl}) and yarnRcOptions.npmRegistryServer (${yarnRcOptions.npmRegistryServer}) to different values.`);
}
}
/** See https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored */
configureYarnBerryGitignore(zeroInstalls) {
const { gitignore } = this.project;
// These patterns are the same whether or not you're using zero-installs
gitignore.exclude(".yarn/*");
gitignore.include(".yarn/patches", ".yarn/plugins", ".yarn/releases", ".yarn/sdks", ".yarn/versions");
if (zeroInstalls) {
gitignore.include("!.yarn/cache");
}
else {
gitignore.exclude(".pnp.*");
}
}
}
exports.NodePackage = NodePackage;
_a = JSII_RTTI_SYMBOL_1;
NodePackage[_a] = { fqn: "projen.javascript.NodePackage", version: "0.99.51" };
/**
* The node package manager to use.
*/
var NodePackageManager;
(function (NodePackageManager) {
/**
* Use `yarn` as the package manager.
*
* @deprecated For `yarn` 1.x use `YARN_CLASSIC` for `yarn` >= 2 use `YARN_BERRY`. Currently, `NodePackageManager.YARN` means `YARN_CLASSIC`. In the future, we might repurpose it to mean `YARN_BERRY`.
*/
NodePackageManager["YARN"] = "yarn";
/**
* Use `yarn` versions >= 2 as the package manager.
*
* @deprecated use YARN_BERRY instead
*/
NodePackageManager["YARN2"] = "yarn2";
/**
* Use `yarn` 1.x as the package manager.
*/
NodePackageManager["YARN_CLASSIC"] = "yarn_classic";
/**
* Use `yarn` versions >= 2 as the package manager.
*/
NodePackageManager["YARN_BERRY"] = "yarn_berry";
/**
* Use `npm` as the package manager.
*/
NodePackageManager["NPM"] = "npm";
/**
* Use `pnpm` as the package manager.
*/
NodePackageManager["PNPM"] = "pnpm";
/**
* Use `bun` as the package manager
*/
NodePackageManager["BUN"] = "bun";
})(NodePackageManager || (exports.NodePackageManager = NodePackageManager = {}));
/**
* Npm package access level
*/
var NpmAccess;
(function (NpmAccess) {
/**
* Package is public.
*/
NpmAccess["PUBLIC"] = "public";
/**
* Package can only be accessed with credentials.
*/
NpmAccess["RESTRICTED"] = "restricted";
})(NpmAccess || (exports.NpmAccess = NpmAccess = {}));
/**
* Why a dependency install was triggered during synthesis.
*/
var InstallReason;
(function (InstallReason) {
/** The node_modules directory does not exist. */
InstallReason["NO_NODE_MODULES"] = "node_modules is missing";
/** The package.json file was modified during synthesis. */
InstallReason["PACKAGE_JSON_CHANGED"] = "package.json has changed";
/** Wildcard dependency versions were resolved to concrete ranges. */
InstallReason["DEPS_RESOLVED"] = "resolved dependency versions changed";
})(InstallReason || (exports.InstallReason = InstallRe