projen
Version:
CDK for software projects
1,147 lines • 179 kB
JavaScript
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
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";
/**
* 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);
}
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(options),
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,
};
// Configure Yarn Berry if using
if (this.packageManager === NodePackageManager.YARN_BERRY ||
this.packageManager === NodePackageManager.YARN2) {
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.pnpmVersion = options.pnpmVersion ?? "10";
this.bunVersion = options.bunVersion ?? "latest";
this.addNodeEngine();
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 `npx projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `yarn
* add/upgrade`. 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 `npx projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `yarn
* add/upgrade`. 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 `npx projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `yarn
* add/upgrade`. 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 `npx projen` run and the version will be recorded
* in your `package.json` file. You can upgrade manually or using `yarn
* add/upgrade`. 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 `yarn 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();
// only run "install" if package.json has changed or if we don't have a
// `node_modules` directory.
if (this.file.changed ||
!(0, fs_1.existsSync)((0, path_1.join)(this.project.outdir, "node_modules"))) {
this.installDependencies();
}
// resolve "*" deps in package.json and update it. if it was changed,
// install deps again so that lockfile is updated.
if (this.resolveDepsAndWritePackageJson()) {
this.installDependencies();
}
}
/**
* 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
* update `package.json` if needed.
*
* @returns `true` if package.json was updated or `false` if not.
*/
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 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) {
this.project.logger.verbose(`${name}: ${currentDefinition} => ${desiredVersion}`);
}
result[name] = desiredVersion;
}
// print removed packages
for (const name of Object.keys(current)) {
if (!result[name]) {
this.project.logger.verbose(`${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 false;
}
(0, util_2.writeFile)(rootPackageJson, updated);
return true;
}
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(options) {
const autoAdd = options.addPackageManagerToDevEngines !== false &&
this._packageManagerExplicit;
const base = autoAdd
? {
packageManager: packageManagerToDevEngine(this.packageManager, options),
}
: {};
const merged = { ...base, ...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() {
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));
for (const task of tasks) {
if (this.scriptsToBeRemoved.has(task.name)) {
continue;
}
result[task.name] = this.npmScriptForTask(task);
}
return {
...result,
...this.scripts,
};
}
npmScriptForTask(task) {
return `${this.project.projenCommand} ${task.name}`;
}
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"));
}
installDependencies() {
this.project.logger.info("Installing dependencies...");
const runtime = new task_runtime_1.TaskRuntime(this.project.outdir);
const taskToRun = this.isAutomatedBuild
? this.installCiTask
: this.installTask;
runtime.runTask(taskToRun.name);
}
configureYarnBerry(project, options) {
const { version = "4.13.0", yarnRcOptions = {}, zeroInstalls = false, } = options.yarnBerryOptions || {};
this.checkForConflictingYarnOptions(yarnRcOptions);
// Set the `packageManager` field in `package.json` to the version specified. This tells `corepack` which version
// of `yarn` to use.
this.addField("packageManager", `yarn@${version}`);
this.configureYarnBerryGitignore(zeroInstalls);
new yarnrc_1.Yarnrc(project, version, 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.34" };
/**
* 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 = {}));
/**
* Determines if an npm package is "scoped" (i.e. it starts with "xxx@").
*/
function isScoped(packageName) {
return packageName.includes("@");
}
function defaultNpmAccess(packageName) {
return isScoped(packageName) ? NpmAccess.RESTRICTED : NpmAccess.PUBLIC;
}
function defaultNpmToken(npmToken, registry) {
// if we are publishing to AWS CodeArtifact, no NPM_TOKEN used (will be requested using AWS CLI later).
if ((0, release_1.isAwsCodeArtifactRegistry)(registry)) {
return undefined;
}
// if we are publishing to GitHub Packages, default to GITHUB_TOKEN.
const isGitHubPackages = registry === GITHUB_PACKAGES_REGISTRY;
return (npmToken ??
(isGitHubPackages ? DEFAULT_GITHUB_TOKEN_SECRET : DEFAULT_NPM_TOKEN_SECRET));
}
function determineLockfile(packageManager) {
switch (packageManager) {
case NodePackageManager.YARN:
case NodePackageManager.YARN_CLASSIC:
case NodePackageManager.YARN2:
case NodePackageManager.YARN_BERRY:
return "yarn.lock";
case NodePackageManager.NPM:
return "package-lock.json";
case NodePackageManager.PNPM:
return "pnpm-lock.yaml";
case NodePackageManager.BUN:
return "bun.lockb";
default:
throw new Error(`unsupported package manager ${packageManager}`);
}
}
/**
* Maps a package manager name (as used in package.json `packageManager` and
* `devEngines.packageManager` fields) to a `NodePackageManager` enum value.
*/
function packageManagerNameToEnum(name, majorVersion) {
switch (name) {
case "npm":
return NodePackageManager.NPM;
case "pnpm":
return NodePackageManager.PNPM;
case "bun":
return NodePackageManager.BUN;
case "yarn":
if (majorVersion !== undefined && majorVersion >= 2) {
return NodePackageManager.YARN_BERRY;
}
return NodePackageManager.YARN_CLASSIC;
default:
return undefined;
}
}
/**
* Maps a `NodePackageManager` to a `DevEngineDependency` with appropriate
* version constraints. Yarn classic and berry get version ranges to
* distinguish them.
*/
function packageManagerToDevEngine(pm, options = {}) {
const onFail = "ignore";
switch (pm) {
case NodePackageManager.YARN:
case NodePackageManager.YARN_CLASSIC:
return { name: "yarn", version: "<2.0.0", onFail };
case NodePackageManager.YARN2:
case NodePackageManager.YARN_BERRY: {
const version = options.yarnBerryOptions?.version ?? "4.0.1";
return { name: "yarn", version: `>=${version}`, onFail };
}
case NodePackageManager.NPM:
return { name: "npm", onFail };
case NodePackageManager.PNPM:
return { name: "pnpm", onFail };
case NodePackageManager.BUN:
return { name: "bun", onFail };
}
}
/**
* Infers the `NodePackageManager` from an existing `package.json`.
*
* Checks in order:
* 1. `packageManager` field (corepack, e.g. `"yarn@4.1.0"`)
* 2. `devEngines.packageManager` field (single entry only; arrays are
* ambiguous and skipped unless a lockfile disambiguates)
*
* @returns the inferred package manager, or `undefined`
*/
function inferPackageManagerFromPkgJson(prev, outdir) {
if (!prev) {
return undefined;
}
// 1. Corepack packageManager field: "yarn@4.1.0"
const corepack = prev.packageManager;
if (corepack) {
const match = corepack.match(/^(\w+)@(\d+)/);
if (match) {
const result = packageManagerNameToEnum(match[1], Number(match[2]));
if (result) {
return result;
}
}
}
// 2. devEngines.packageManager
const devEnginesPm = prev.devEngines?.packageManager;
if (devEnginesPm) {
if (!Array.isArray(devEnginesPm)) {
// Single entry
return packageManagerNameToEnum(devEnginesPm.name);
}
if (devEnginesPm.length === 1) {
return packageManagerNameToEnum(devEnginesPm[0].name);
}
// Multiple entries — try to disambiguate with lockfile
if (devEnginesPm.length > 1) {
const fromLockfile = inferPackageManagerFromLockfile(outdir);
if (fromLockfile) {
return fromLockfile;
}
}
}
return undefined;
}
/**
* Infers the `NodePackageManager` from the presence of a lockfile.
*/
function inferPackageManagerFromLockfile(outdir) {
if ((0, fs_1.existsSync)((0, path_1.join)(outdir, "pnpm-lock.yaml"))) {
return NodePackageManager.PNPM;
}
if ((0, fs_1.existsSync)((0, path_1.join)(outdir, "bun.lockb"))) {
return NodePackageManager.BUN;
}
if ((0, fs_1.existsSync)((0, path_1.join)(outdir, "yarn.lock"))) {
return NodePackageManager.YARN_CLASSIC;
}
if ((0, fs_1.existsSync)((0, path_1.join)(outdir, "package-lock.json"))) {
return NodePackageManager.NPM;
}
return undefined;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibm9kZS1wYWNrYWdlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2phdmFzY3JpcHQvbm9kZS1wYWNrYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7QUEwNURBLDBDQWVDOztBQXo2REQsMkJBT1k7QUFDWiwrQkFBcUM7QUFDckMsaUNBQWlDO0FBQ2pDLGlDQUlnQjtBQUNoQixxQ0FBaUQ7QUFDakQsMENBQXFEO0FBQ3JELDRDQUF5QztBQUN6QyxrREFBaUQ7QUFDakQsa0NBQW1DO0FBRW5DLHdDQUF1RDtBQUV2RCxrREFBOEM7QUFDOUMsa0NBQThFO0FBRTlFLE1BQU0sVUFBVSxHQUFHLFlBQVksQ0FBQztBQUNoQyxNQUFNLHdCQUF3QixHQUFHLDZCQUE2QixDQUFDO0FBQy9ELE1BQU0sd0JBQXdCLEdBQUcsb0JBQW9CLENBQUM7QUFDdEQsTUFBTSx3QkFBd0IsR0FBRyxXQUFXLENBQUM7QUFDN0MsTUFBTSwyQkFBMkIsR0FBRyxjQUFjLENBQUM7QUEyYW5EOztHQUVHO0FBQ0gsSUFBWSx3QkFhWDtBQWJELFdBQVksd0JBQXdCO0lBQ2xDOztPQUVHO0lBQ0gscUZBQXlELENBQUE7SUFFekQ7Ozs7O09BS0c7SUFDSCx1REFBMkIsQ0FBQTtBQUM3QixDQUFDLEVBYlcsd0JBQXdCLHdDQUF4Qix3QkFBd0IsUUFhbkM7QUE4REQ7O0dBRUc7QUFDSCxNQUFhLFdBQVksU0FBUSxxQkFBUztJQUN4Qzs7Ozs7T0FLRztJQUNJLE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBZ0I7UUFDL0IsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFZLEVBQW9CLEVBQUUsQ0FBQyxDQUFDLFlBQVksV0FBVyxDQUFDO1FBQzFFLE9BQU8sT0FBTyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDdkMsQ0FBQztJQThIRCxZQUFZLE9BQWdCLEVBQUUsVUFBOEIsRUFBRTtRQUM1RCxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7UUFYQSxZQUFPLEdBQTJCLEVBQUUsQ0FBQztRQUNyQyx1QkFBa0IsR0FBRyxJQUFJLEdBQUcsRUFBVSxDQUFDO1FBQ3ZDLGFBQVEsR0FBZ0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUNsQyxRQUFHLEdBQTJCLEVBQUUsQ0FBQztRQUNqQyxZQUFPLEdBQTJCLEVBQUUsQ0FBQztRQVNwRCxJQUFJLENBQUMsV0FBVyxHQUFHLE9BQU8sQ0FBQyxXQUFXLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQztRQUN2RCxJQUFJLENBQUMscUJBQXFCLEdBQUc7WUFDM0IsbUJBQW1CLEVBQUUsSUFBSTtZQUN6QixHQUFHLE9BQU8sQ0FBQyxxQkFBcUI7U0FDakMsQ0FBQztRQUNGLElBQUksQ0FBQyx3QkFBd0IsR0FBRyxPQUFPLENB