feat: new project config to allow invalid licenses
MODIFY
internal/codec-js-manifest/index.ts
internal/codec-js-manifest/index.ts
@@ -9,7 +9,7 @@ import {Consumer} from "@internal/consume";
import {SemverVersionNode, parseSemverVersion} from "@internal/codec-semver";
import {
SPDXExpressionNode,
- parseSPDXLicense,
+ SpdxLicenseParser,
} from "@internal/codec-spdx-license";
import {normalizeDependencies, parseGitDependencyPattern} from "./dependencies";
import {
@@ -28,10 +28,15 @@ import {
import {tryParseWithOptionalOffsetPosition} from "@internal/parser-core";
import {normalizeName} from "./name";
import {descriptions} from "@internal/diagnostics";
-import {RelativeFilePathMap, createRelativeFilePath} from "@internal/path";
+import {
+ AbsoluteFilePath,
+ RelativeFilePathMap,
+ createRelativeFilePath,
+} from "@internal/path";
import {toCamelCase} from "@internal/string-utils";
import {PathPatterns, parsePathPattern} from "@internal/path-match";
import {normalizeCompatManifest} from "@internal/codec-js-manifest/compat";
+import {ProjectDefinition} from "@internal/project";
export * from "./types";
@@ -177,6 +182,7 @@ const INVALID_IGNORE_LICENSES = [
function normalizeLicense(
consumer: Consumer,
loose: boolean,
+ projects: Array<ProjectDefinition>,
): undefined | SPDXExpressionNode {
if (!consumer.has("license")) {
return undefined;
@@ -209,6 +215,11 @@ function normalizeLicense(
}
// Parse as a SPDX expression
+ const spdxLisenceCode = new SpdxLicenseParser({
+ packageVersion: consumer.get("version").asString(),
+ packageName: consumer.get("name").asString(),
+ projects,
+ });
return tryParseWithOptionalOffsetPosition(
{
loose,
@@ -217,7 +228,7 @@ function normalizeLicense(
},
{
getOffsetPosition: () => licenseProp.getLocation("inner-value").start,
- parse: (opts) => parseSPDXLicense(opts),
+ parse: (opts) => spdxLisenceCode.parse(opts),
},
);
}
@@ -612,7 +623,11 @@ function checkDependencyKeyTypo(key: string, prop: Consumer) {
}
}
-export async function normalizeManifest(consumer: Consumer): Promise<Manifest> {
+export async function normalizeManifest(
+ path: AbsoluteFilePath,
+ consumer: Consumer,
+ projects: Array<ProjectDefinition>,
+): Promise<Manifest> {
const loose =
consumer.path !== undefined &&
consumer.path.getSegments().includes("node_modules");
@@ -643,7 +658,7 @@ export async function normalizeManifest(consumer: Consumer): Promise<Manifest> {
version,
private: normalizeBoolean(consumer, "private") === true,
description: normalizeString(consumer, "description"),
- license: normalizeLicense(consumer, loose),
+ license: normalizeLicense(consumer, loose, projects),
type: consumer.get("type").asStringSetOrVoid(["module", "commonjs"]),
bin: normalizeBin(consumer, name.packageName, loose),
scripts: normalizeStringMap(consumer, "scripts", loose),

ADD
internal/codec-spdx-license/SpdxLicenseParser.ts
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {
+ BaseTokens,
+ ParserCore,
+ ParserOptions,
+ SimpleToken,
+ ValueToken,
+ createParser,
+ isAlpha,
+ isDigit,
+} from "@internal/parser-core";
+import {getSPDXLicense, licenseNames} from "./index";
+import {descriptions} from "@internal/diagnostics";
+import {Number0} from "@internal/ob1";
+import {ProjectDefinition} from "@internal/project";
+import {ExpressionNode, LicenseNode} from "@internal/codec-spdx-license/types";
+import {satisfiesSemver} from "@internal/codec-semver";
+import stringify from "@internal/codec-semver/stringify";
+
+//# Tokens
+type Tokens = BaseTokens & {
+ ParenOpen: SimpleToken<"ParenOpen">;
+ ParenClose: SimpleToken<"ParenClose">;
+ Plus: SimpleToken<"Plus">;
+ And: SimpleToken<"And">;
+ With: SimpleToken<"With">;
+ Or: SimpleToken<"Or">;
+ Word: ValueToken<"Word", string>;
+};
+
+function isWordChar(char: string) {
+ return isAlpha(char) || isDigit(char) || char === "-" || char === ".";
+}
+
+type SPDXLicenseParserOptions = ParserOptions & {
+ loose?: boolean;
+};
+
+type SPDXParserTypes = {
+ tokens: Tokens;
+ state: {};
+ options: SPDXLicenseParserOptions;
+ meta: void;
+};
+
+type SPDXParser = ParserCore<SPDXParserTypes>;
+
+const createSPDXLicenseParser = createParser<SPDXParserTypes>({
+ diagnosticCategory: "parse/spdxLicense",
+ ignoreWhitespaceTokens: true,
+ tokenize(parser: SPDXParser, index: Number0) {
+ const char = parser.getInputCharOnly(index);
+
+ if (char === "+") {
+ return parser.finishToken("Plus");
+ }
+
+ if (char === "(") {
+ return parser.finishToken("ParenOpen");
+ }
+
+ if (char === ")") {
+ return parser.finishToken("ParenClose");
+ }
+
+ if (isWordChar(char)) {
+ const [value, end] = parser.readInputFrom(index, isWordChar);
+
+ if (value === "AND") {
+ return parser.finishToken("And", end);
+ } else if (value === "OR") {
+ return parser.finishToken("Or", end);
+ } else if (value === "WITH") {
+ return parser.finishToken("With", end);
+ } else {
+ return parser.finishValueToken("Word", value, end);
+ }
+ }
+
+ return undefined;
+ },
+});
+
+type LicenseCodec = {
+ packageName: string;
+ packageVersion: string;
+ projects: Array<ProjectDefinition>;
+};
+
+export default class SpdxLicenseParser {
+ private readonly packageName: string;
+ private readonly packageVersion: string;
+ private readonly projects: Array<ProjectDefinition>;
+ constructor({packageName, packageVersion, projects}: LicenseCodec) {
+ this.packageName = packageName;
+ this.packageVersion = packageVersion;
+ this.projects = projects;
+ }
+
+ parseExpression(parser: SPDXParser): ExpressionNode {
+ const startPos = parser.getPosition();
+ const startToken = parser.getToken();
+
+ let value;
+
+ switch (startToken.type) {
+ case "ParenOpen": {
+ parser.nextToken();
+ value = this.parseExpression(parser);
+ parser.expectToken("ParenClose");
+ break;
+ }
+
+ case "Word": {
+ value = this.parseLicense(parser, startToken);
+ break;
+ }
+
+ case "Or":
+ case "And":
+ throw parser.unexpected({
+ description: descriptions.SPDX.OPERATOR_NOT_BETWEEN_EXPRESSION,
+ });
+
+ case "Plus":
+ throw parser.unexpected({
+ description: descriptions.SPDX.PLUS_NOT_AFTER_LICENSE,
+ });
+
+ case "ParenClose":
+ throw parser.unexpected({
+ description: descriptions.SPDX.UNOPENED_PAREN,
+ });
+
+ default:
+ throw parser.unexpected();
+ }
+
+ // Parse and/or
+ const nextToken = parser.getToken();
+ switch (nextToken.type) {
+ case "Or": {
+ parser.nextToken();
+ return {
+ type: "Or",
+ loc: parser.finishLoc(startPos),
+ left: value,
+ right: this.parseExpression(parser),
+ };
+ }
+
+ case "And": {
+ parser.nextToken();
+ return {
+ type: "And",
+ loc: parser.finishLoc(startPos),
+ left: value,
+ right: this.parseExpression(parser),
+ };
+ }
+
+ default:
+ return value;
+ }
+ }
+
+ isLicenseValid(
+ id: string,
+ ): {
+ inConfig: boolean;
+ satisfiesVersion: boolean;
+ packageVersionInConfig: string;
+ } {
+ let inConfig = false;
+ let satisfiesVersion = false;
+ let packageVersionInConfig = "";
+ for (const project of this.projects) {
+ const {invalidLicenses} = project.config.dependencies.exceptions;
+ const licenses = invalidLicenses.get(id);
+ if (licenses) {
+ for (const license of licenses) {
+ satisfiesVersion = satisfiesSemver(this.packageVersion, license.range);
+ inConfig = license.name === this.packageName && satisfiesVersion;
+ // there's at least a version of the dependency that doesn't satisfy the criteria
+ // so exit the loop and communicate to the user
+ if (!satisfiesVersion) {
+ packageVersionInConfig = stringify(license.range);
+ break;
+ }
+ }
+ }
+ }
+
+ return {inConfig, satisfiesVersion, packageVersionInConfig};
+ }
+
+ parseLicense(parser: SPDXParser, token: Tokens["Word"]): LicenseNode {
+ const startPos = parser.getPosition();
+ parser.nextToken();
+
+ // Validate id
+ const id = token.value;
+ let licenseInfo = getSPDXLicense(id);
+ const nextToken = parser.getToken();
+ let possibleCorrectLicense;
+ // Sometimes licenses will be specified as "Apache 2.0" but what they actually meant was "Apache-2.0"
+
+ // In loose mode, just make it equivalent, otherwise, complain
+ if (licenseInfo === undefined && nextToken.type === "Word") {
+ possibleCorrectLicense = `${id}-${nextToken.value}`;
+ const possibleLicenseInfo = getSPDXLicense(possibleCorrectLicense);
+
+ if (possibleLicenseInfo !== undefined) {
+ if (parser.options.loose) {
+ // Just allow it...
+ licenseInfo = possibleLicenseInfo;
+ parser.nextToken();
+ } else {
+ throw parser.unexpected({
+ description: descriptions.SPDX.VALID_LICENSE_WITH_MISSING_DASH(
+ possibleCorrectLicense,
+ ),
+ start: parser.getPositionFromIndex(token.start),
+ end: parser.getPositionFromIndex(nextToken.end),
+ });
+ }
+ }
+ }
+
+ let licenseId: string;
+
+ if (licenseInfo === undefined) {
+ const {inConfig, satisfiesVersion, packageVersionInConfig} = this.isLicenseValid(
+ possibleCorrectLicense || id,
+ );
+
+ if (!inConfig) {
+ if (!satisfiesVersion) {
+ throw parser.unexpected({
+ description: descriptions.SPDX.UNKNOWN_LICENSE_IN_VERSION({
+ packageName: this.packageName,
+ packageVersionInConfig,
+ id: possibleCorrectLicense || id,
+ newPackageVersion: this.packageVersion,
+ }),
+ start: parser.getPositionFromIndex(token.start),
+ end: parser.getPositionFromIndex(token.end),
+ });
+ }
+ throw parser.unexpected({
+ description: descriptions.SPDX.UNKNOWN_LICENSE({
+ id: possibleCorrectLicense || id,
+ knownLicenses: licenseNames,
+ packageName: this.packageName,
+ packageVersion: this.packageVersion,
+ }),
+ start: parser.getPositionFromIndex(token.start),
+ end: parser.getPositionFromIndex(token.end),
+ });
+ }
+ licenseId = id;
+ // allow the license because it's inside the config
+ parser.nextToken();
+ } else {
+ licenseId = licenseInfo.licenseId;
+ }
+
+ // Is this a plus? (wtf is this)
+ const plus = parser.eatToken("Plus") !== undefined;
+
+ // Get exception
+ let exception;
+ if (parser.eatToken("With")) {
+ const token = parser.getToken();
+ if (token.type === "Word") {
+ exception = token.value;
+ parser.nextToken();
+ } else {
+ throw parser.unexpected({
+ description: descriptions.SPDX.WITH_RIGHT_LICENSE_ONLY,
+ });
+ }
+ }
+
+ return {
+ type: "License",
+ loc: parser.finishLoc(startPos),
+ id: licenseId,
+ exception,
+ plus,
+ };
+ }
+
+ parse(opts: SPDXLicenseParserOptions): ExpressionNode {
+ const parser = createSPDXLicenseParser(opts);
+ const expr = this.parseExpression(parser);
+ parser.finalize();
+ return expr;
+ }
+}

MODIFY
internal/codec-spdx-license/index.ts
internal/codec-spdx-license/index.ts
@@ -28,11 +28,11 @@ for (const license of data.licenses) {
export {licenseNames};
-export {ExpressionNode as SPDXExpressionNode} from "./parse";
+export {ExpressionNode as SPDXExpressionNode} from "./types";
export function getSPDXLicense(licenseId: string): undefined | License {
return idToLicense.get(licenseId.toLowerCase());
}
-export {default as parseSPDXLicense} from "./parse";
export {default as stringifySPDXLicense} from "./stringify";
+export {default as SpdxLicenseParser} from "./SpdxLicenseParser";

MODIFY
internal/codec-spdx-license/parse.ts
internal/codec-spdx-license/parse.ts
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-import {
- BaseTokens,
- ComplexNode,
- ParserCore,
- ParserOptions,
- SimpleToken,
- ValueToken,
- createParser,
- isAlpha,
- isDigit,
-} from "@internal/parser-core";
-import {getSPDXLicense, licenseNames} from "./index";
-import {descriptions} from "@internal/diagnostics";
-import {Number0} from "@internal/ob1";
-
-//# Tokens
-type Tokens = BaseTokens & {
- ParenOpen: SimpleToken<"ParenOpen">;
- ParenClose: SimpleToken<"ParenClose">;
- Plus: SimpleToken<"Plus">;
- And: SimpleToken<"And">;
- With: SimpleToken<"With">;
- Or: SimpleToken<"Or">;
- Word: ValueToken<"Word", string>;
-};
-
-//# Nodes
-export type ExpressionNode = LicenseNode | AndNode | OrNode;
-
-type AndNode = ComplexNode<
- "And",
- {
- left: ExpressionNode;
- right: ExpressionNode;
- }
->;
-
-type OrNode = ComplexNode<
- "Or",
- {
- left: ExpressionNode;
- right: ExpressionNode;
- }
->;
-
-type LicenseNode = ComplexNode<
- "License",
- {
- plus: boolean;
- id: string;
- exception: undefined | string;
- }
->;
-
-function isWordChar(char: string) {
- return isAlpha(char) || isDigit(char) || char === "-" || char === ".";
-}
-
-type SPDXLicenseParserOptions = ParserOptions & {
- loose?: boolean;
-};
-
-type SPDXParserTypes = {
- tokens: Tokens;
- state: {};
- options: SPDXLicenseParserOptions;
- meta: void;
-};
-
-type SPDXParser = ParserCore<SPDXParserTypes>;
-
-const createSPDXLicenseParser = createParser<SPDXParserTypes>({
- diagnosticCategory: "parse/spdxLicense",
- ignoreWhitespaceTokens: true,
- tokenize(parser: SPDXParser, index: Number0) {
- const char = parser.getInputCharOnly(index);
-
- if (char === "+") {
- return parser.finishToken("Plus");
- }
-
- if (char === "(") {
- return parser.finishToken("ParenOpen");
- }
-
- if (char === ")") {
- return parser.finishToken("ParenClose");
- }
-
- if (isWordChar(char)) {
- const [value, end] = parser.readInputFrom(index, isWordChar);
- const upperCasedValue = value.toUpperCase();
-
- if (upperCasedValue === "AND") {
- return parser.finishToken("And", end);
- } else if (upperCasedValue === "OR") {
- return parser.finishToken("Or", end);
- } else if (upperCasedValue === "WITH") {
- return parser.finishToken("With", end);
- } else {
- return parser.finishValueToken("Word", value, end);
- }
- }
-
- return undefined;
- },
-});
-
-function parseLicense(parser: SPDXParser, token: Tokens["Word"]): LicenseNode {
- const startPos = parser.getPosition();
- parser.nextToken();
-
- // Validate id
- const id = token.value;
- let licenseInfo = getSPDXLicense(id);
- const nextToken = parser.getToken();
-
- // Sometimes licenses will be specified as "Apache 2.0" but what they actually meant was "Apache-2.0"
-
- // In loose mode, just make it equivalent, otherwise, complain
- if (licenseInfo === undefined && nextToken.type === "Word") {
- const possibleCorrectLicense = `${id}-${nextToken.value}`;
- const possibleLicenseInfo = getSPDXLicense(possibleCorrectLicense);
-
- if (possibleLicenseInfo !== undefined) {
- if (parser.options.loose) {
- // Just allow it...
- licenseInfo = possibleLicenseInfo;
- parser.nextToken();
- } else {
- throw parser.unexpected({
- description: descriptions.SPDX.VALID_LICENSE_WITH_MISSING_DASH(
- possibleCorrectLicense,
- ),
- start: parser.getPositionFromIndex(token.start),
- end: parser.getPositionFromIndex(nextToken.end),
- });
- }
- }
- }
-
- if (licenseInfo === undefined) {
- throw parser.unexpected({
- description: descriptions.SPDX.UNKNOWN_LICENSE(id, licenseNames),
- start: parser.getPositionFromIndex(token.start),
- end: parser.getPositionFromIndex(token.end),
- });
- }
-
- // Is this a plus? (wtf is this)
- const plus = parser.eatToken("Plus") !== undefined;
-
- // Get exception
- let exception;
- if (parser.eatToken("With")) {
- const token = parser.getToken();
- if (token.type === "Word") {
- exception = token.value;
- parser.nextToken();
- } else {
- throw parser.unexpected({
- description: descriptions.SPDX.WITH_RIGHT_LICENSE_ONLY,
- });
- }
- }
-
- return {
- type: "License",
- loc: parser.finishLoc(startPos),
- id: licenseInfo.licenseId,
- exception,
- plus,
- };
-}
-
-function parseExpression(parser: SPDXParser): ExpressionNode {
- const startPos = parser.getPosition();
- const startToken = parser.getToken();
-
- let value;
-
- switch (startToken.type) {
- case "ParenOpen": {
- parser.nextToken();
- value = parseExpression(parser);
- parser.expectToken("ParenClose");
- break;
- }
-
- case "Word": {
- value = parseLicense(parser, startToken);
- break;
- }
-
- case "Or":
- case "And":
- throw parser.unexpected({
- description: descriptions.SPDX.OPERATOR_NOT_BETWEEN_EXPRESSION,
- });
-
- case "Plus":
- throw parser.unexpected({
- description: descriptions.SPDX.PLUS_NOT_AFTER_LICENSE,
- });
-
- case "ParenClose":
- throw parser.unexpected({
- description: descriptions.SPDX.UNOPENED_PAREN,
- });
-
- default:
- throw parser.unexpected();
- }
-
- // Parse and/or
- const nextToken = parser.getToken();
- switch (nextToken.type) {
- case "Or": {
- parser.nextToken();
- return {
- type: "Or",
- loc: parser.finishLoc(startPos),
- left: value,
- right: parseExpression(parser),
- };
- }
-
- case "And": {
- parser.nextToken();
- return {
- type: "And",
- loc: parser.finishLoc(startPos),
- left: value,
- right: parseExpression(parser),
- };
- }
-
- default:
- return value;
- }
-}
-
-export default function parse(opts: SPDXLicenseParserOptions): ExpressionNode {
- const parser = createSPDXLicenseParser(opts);
- const expr = parseExpression(parser);
- parser.finalize();
- return expr;
-}

MODIFY
internal/codec-spdx-license/stringify.ts
internal/codec-spdx-license/stringify.ts
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
-
-import {ExpressionNode} from "./parse";
+import {ExpressionNode} from "@internal/codec-spdx-license/types";
export default function stringify(node: ExpressionNode): string {
// TODO parens

ADD
internal/codec-spdx-license/types.ts
+import {ComplexNode} from "@internal/parser-core";
+
+export type AndNode = ComplexNode<
+ "And",
+ {
+ left: ExpressionNode;
+ right: ExpressionNode;
+ }
+>;
+
+export type OrNode = ComplexNode<
+ "Or",
+ {
+ left: ExpressionNode;
+ right: ExpressionNode;
+ }
+>;
+
+export type LicenseNode = ComplexNode<
+ "License",
+ {
+ plus: boolean;
+ id: string;
+ exception: undefined | string;
+ }
+>;
+
+//# Nodes
+export type ExpressionNode = LicenseNode | AndNode | OrNode;

MODIFY
internal/core/server/commands/init.ts
internal/core/server/commands/init.ts
@@ -177,9 +177,14 @@ export default createServerCommand<Flags>({
//
const manifestPath = cwd.append("package.json");
let manifest: undefined | Manifest;
+ const projects = await server.projectManager.getProjectHierarchyFromPath(
+ manifestPath,
+ );
if (await exists(manifestPath)) {
manifest = await normalizeManifest(
+ manifestPath,
consumeJSON(await readFileTextMeta(manifestPath)),
+ projects,
);
}

MODIFY
internal/core/server/fs/MemoryFileSystem.ts
internal/core/server/fs/MemoryFileSystem.ts
@@ -653,8 +653,11 @@ export default class MemoryFileSystem {
consumeDiagnosticCategory: "parse/manifest",
});
+ const projects = await this.server.projectManager.getProjectHierarchyFromPath(
+ path,
+ );
const {consumer: normalizeConsumer, diagnostics: rawDiagnostics} = consumer.capture();
- const manifest = await normalizeManifest(normalizeConsumer);
+ const manifest = await normalizeManifest(path, normalizeConsumer, projects);
// If manifest is undefined then we failed to validate and have diagnostics
if (rawDiagnostics.length > 0) {

MODIFY
internal/core/server/project/ProjectManager.ts
internal/core/server/project/ProjectManager.ts
@@ -816,6 +816,21 @@ export default class ProjectManager {
return this.projectDirectoryToProject.get(path);
}
+ /**
+ * Given a path, it returns the list of projects
+ * @param path
+ */
+ public getProjectHierarchyFromPath(
+ path: AbsoluteFilePath,
+ ): Array<ProjectDefinition> {
+ const found = this.findLoadedProject(path);
+ if (found === undefined) {
+ return [];
+ } else {
+ return this.getHierarchyFromProject(found);
+ }
+ }
+
public findLoadedProject(
path: AbsoluteFilePath,
): undefined | ProjectDefinition {

MODIFY
internal/diagnostics/descriptions/parsers/spdx.ts
internal/diagnostics/descriptions/parsers/spdx.ts
@@ -3,8 +3,30 @@ import {markup} from "@internal/markup";
import {buildSuggestionAdvice} from "../../helpers";
// @internal/codec-spdx-license
+
+type UnknownLicense = {
+ id: string;
+ knownLicenses: Array<string>;
+ packageName: string;
+ packageVersion: string;
+};
+
+type UnknowLicenseInVersion = {
+ id: string;
+ packageVersionInConfig: string;
+ newPackageVersion: string;
+ packageName: string;
+};
+
export const spdx = createDiagnosticsCategory({
- UNKNOWN_LICENSE: (id: string, knownLicenses: Array<string>) => ({
+ UNKNOWN_LICENSE: (
+ {
+ id,
+ knownLicenses,
+ packageName,
+ packageVersion,
+ }: UnknownLicense,
+ ) => ({
message: markup`Unknown license <emphasis>${id}</emphasis>`,
advice: [
...buildSuggestionAdvice(id, knownLicenses, {ignoreCase: true}),
@@ -13,6 +35,37 @@ export const spdx = createDiagnosticsCategory({
category: "info",
text: markup`The <emphasis>SPDX</emphasis> registry is used to ensure valid and legal licenses. See <hyperlink target="https://spdx.org/licenses/" /> for more information.`,
},
+ {
+ type: "log",
+ category: "info",
+ text: markup`To automatically add an exception for this license, run:`,
+ },
+ {
+ type: "command",
+ command: `rome config push dependencies.exceptions.invalidLicenses.${id} "${packageName}@${packageVersion}"`,
+ },
+ ],
+ }),
+
+ UNKNOWN_LICENSE_IN_VERSION: (
+ {
+ id,
+ packageName,
+ packageVersionInConfig,
+ newPackageVersion,
+ }: UnknowLicenseInVersion,
+ ) => ({
+ message: markup`The dependency <emphasis>${packageName}@${newPackageVersion}</emphasis> doesn't satisfy the version inside your configuration file <emphasis>(${packageVersionInConfig})</emphasis>.`,
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: markup`To automatically update the exception for this license, run:`,
+ },
+ {
+ type: "command",
+ command: `rome config set dependencies.exceptions.invalidLicenses.${id} "${packageName}@${newPackageVersion}"`,
+ },
],
}),
VALID_LICENSE_WITH_MISSING_DASH: (possibleCorrectLicense: string) => ({

MODIFY
internal/project/load.ts
internal/project/load.ts
// Project configs are initialized very infrequently anyway so we can live with the extremely minor perf hit.
import {Consumer} from "@internal/consume";
import {
+ InvalidLicenses,
PartialProjectConfig,
ProjectConfig,
ProjectConfigMeta,
@@ -240,7 +241,34 @@ export async function normalizeProjectConfig(
const dependencies = consumer.get("dependencies");
if (categoryExists(dependencies)) {
if (dependencies.has("enabled")) {
- config.dependencies.enabled = dependencies.get("dependencies").asBoolean();
+ config.dependencies.enabled = dependencies.get("enabled").asBoolean();
+ }
+
+ if (dependencies.has("exceptions")) {
+ const exceptions = dependencies.get("exceptions").asMap();
+
+ const invalidLicenses = exceptions.get("invalidLicenses");
+ if (invalidLicenses) {
+ let licenses: InvalidLicenses = new Map();
+ for (const [name, packages] of invalidLicenses.asMap()) {
+ licenses.set(
+ name,
+ packages.asMappedArray((c) => {
+ const packageValue = c.asString();
+ // inside the config, we store packageName@version
+ const {0: name, 1: version} = packageValue.split("@");
+ return {
+ name,
+ // we might not have the version, so we assume that it is the latest
+ range: parseSemverRange({input: version || "latest"}),
+ };
+ }),
+ );
+ }
+ config.dependencies.exceptions = {
+ invalidLicenses: licenses,
+ };
+ }
}
}

MODIFY
internal/project/types.ts
internal/project/types.ts
@@ -30,6 +30,17 @@ export type ProjectDefinition = {
initialized: boolean;
};
+export type InvalidLicenses = Map<
+ string,
+ Array<{
+ name: string;
+ range: SemverRangeNode;
+ }>
+>;
+export type DependenciesExceptions = {
+ invalidLicenses: InvalidLicenses;
+};
+
// Project config objects to categorize settings
export type ProjectConfigObjects = {
cache: {};
@@ -62,6 +73,7 @@ export type ProjectConfigObjects = {
};
dependencies: {
enabled: boolean;
+ exceptions: DependenciesExceptions;
};
targets: Map<string, ProjectConfigTarget>;
};
@@ -142,6 +154,9 @@ export function createDefaultProjectConfig(): ProjectConfig {
},
dependencies: {
enabled: false,
+ exceptions: {
+ invalidLicenses: new Map(),
+ },
},
lint: {
ignore: [],

MODIFY
website/src/_includes/docs/project-config.md
website/src/_includes/docs/project-config.md
@@ -18,6 +18,13 @@ lint: {
ignore: []
globals: []
}
+dependencies: {
+ exceptions: {
+ invalidLicenses: {
+ "funky-licence": ["lib@1.0.0", "lib@1.1.0", "other-lib@2.0.0"]
+ }
+ }
+}
```
### Properties
@@ -75,6 +82,23 @@ Custom variables you want to declare as global.
rome config push lint.globals SomeGlobal
```
+### `dependencies.exceptions`
+
+Exception rules for your dependencies that don't pass validation.
+
+#### `dependencies.exceptions.invalidLicenses`
+
+Sometimes Rome might complain that one or more of your dependencies has an invalid license.
+
+Optionally, you can insert the name of this invalid license here:
+
+```bash
+rome config push dependencies.exceptions.invalidLicenses.invalid-license-name "third-party-lib@0.1.0"
+```
+
+If you are unsure about the license name of your library, rome will suggest the command for
+you when you try to run a command.
+
### Supported Locations
You can specify your project config in a few different places.

feat: new project config to allow invalid licenses (#1131)

Co-authored-by: Sebastian <853712+sebmck@users.noreply.github.com>