Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 68 additions & 42 deletions .release-it.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,71 @@ const types = new Map([
const normalizeRepoUrl = url => url.replace(/^git\+/, "").replace(/\.git$/, "");
const repoUrl = pkg?.repository?.url ? normalizeRepoUrl(pkg.repository.url) : null;

module.exports = () => {
const breakingChangePattern = /\bBREAKING[ -]?CHANGE\b/i;

function hasBreakingChange(commit) {
if (commit.breaking) {
return true;
}

const type = String(commit.type || "").trim();

if (type.endsWith("!")) {
return true;
}

if (typeof commit.header === "string" && /^\w+(?:\([^)]+\))?!:/.test(commit.header)) {
return true;
}

if (
commit.notes?.some(note =>
[note.title, note.text].some(value => typeof value === "string" && breakingChangePattern.test(value))
)
) {
return true;
}

return typeof commit.footer === "string" && breakingChangePattern.test(commit.footer);
}

function whatBump(commits, currentVersion = pkg.version) {
let isBreaking = false;
let isMinor = false;
let isPatch = false;

for (const commit of commits) {
if (hasBreakingChange(commit)) {
isBreaking = true;
}

const type = String(commit.type || "")
.trim()
.toLowerCase()
.replace(/!+$/, "");

if (["feat", "revert"].includes(type)) {
isMinor = true;
}

if (["fix", "perf", "refactor", "ci"].includes(type)) {
isPatch = true;
}
}

if (isBreaking) {
const currentMajor = Number.parseInt(String(currentVersion).replace(/^v/i, "").split(".")[0], 10);

return {level: Number.isNaN(currentMajor) || currentMajor >= 1 ? 0 : 1};
}

if (isMinor) return {level: 1};
if (isPatch) return {level: 2};

return null;
}

const createReleaseConfig = () => {
const contributors = getContributors();

return {
Expand Down Expand Up @@ -151,14 +215,6 @@ module.exports = () => {

presetConfig: {
types: [...types.entries()].map(([type, section]) => ({type, section, hidden: false})),
releaseRules: [
{breaking: true, release: "major"},
{type: "feat", release: "minor"},
{type: "fix", release: "patch"},
{type: "perf", release: "patch"},
{type: "refactor", release: "patch"},
{type: "ci", release: "patch"},
],
},

context: {
Expand All @@ -168,39 +224,7 @@ module.exports = () => {
contributors,
},

recommendedBump: true,
whatBump: commits => {
let isMajor = false;
let isMinor = false;
let isPatch = false;

for (const commit of commits) {
const hasBreaking =
Boolean(commit.breaking) ||
(commit.notes &&
commit.notes.some(n => /BREAKING[ -]CHANGE/i.test(n.title || n.text || "")));
if (hasBreaking) {
isMajor = true;
break;
}

const type = (commit.type || "").toLowerCase().replace(/!+$/, "");

if (type === "feat") {
isMinor = true;
}

if (["fix", "perf", "refactor", "ci"].includes(type)) {
isPatch = true;
}
}

if (isMajor) return {level: 0};
if (isMinor) return {level: 1};
if (isPatch) return {level: 2};

return null;
},
whatBump,
writerOpts: {
headerPartial:
"## 🚀 Release {{#if name}}`{{name}}` {{else}}{{#if @root.pkg}}`{{@root.pkg.name}}` {{/if}}{{/if}}v{{version}} ({{date}})\n\n",
Expand Down Expand Up @@ -259,3 +283,5 @@ module.exports = () => {
},
};
};

module.exports = Object.assign(createReleaseConfig, {whatBump});
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Repository conventions

## TypeScript file naming

- Use PascalCase filenames only when the file's primary export is a class, for example `LocaleFinder.ts`.
- Use camelCase filenames for modules that export helper functions, utilities, constants, enums, or types without a primary class, for example `utils/filePrecedence.ts`.
- Keep finder-specific helper modules in `src/cli/entrypoint/finder/utils`.
37 changes: 26 additions & 11 deletions src/cli/entrypoint/finder/AbstractAssetFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from "fs";
import {getAppPath, getAppSourcePath, getResolvePath, getSharedPath, getSourcePath} from "@cli/resolvers/path";

import AbstractFinder from "./AbstractFinder";
import {FileLayer, getWorkspaceFileLayers, setFilePrecedence, type WorkspaceFileLayer} from "./utils/filePrecedence";

import {EntrypointFile} from "@typing/entrypoint";

Expand Down Expand Up @@ -35,26 +36,40 @@ export default abstract class extends AbstractFinder {
}

protected async getFiles(): Promise<Set<EntrypointFile>> {
const files = new Set<EntrypointFile>();
const files = new Map<string, EntrypointFile>();

const collect = async (directory: string, layer: FileLayer): Promise<void> => {
const assetFiles = await this.findFiles(getResolvePath(directory));

const parser = async (directory: string): Promise<void> => {
if (files.size === 0 || this.canMerge()) {
const localeFiles = await this.findFiles(getResolvePath(directory));
for (const file of assetFiles) {
const canonicalPath = fs.realpathSync.native(file.file);

for (const file of localeFiles) {
files.add(file);
if (!files.has(canonicalPath)) {
files.set(canonicalPath, setFilePrecedence(file, {layer}));
}
}
};

const dir = this.getDirectory();

await parser(getAppSourcePath(this.config, dir));
await parser(getAppPath(this.config, dir));
await parser(getSharedPath(this.config, dir));
await parser(getSourcePath(this.config, dir));
const directories: Record<WorkspaceFileLayer, string> = {
[FileLayer.Source]: getSourcePath(this.config, dir),
[FileLayer.Shared]: getSharedPath(this.config, dir),
[FileLayer.App]: getAppPath(this.config, dir),
[FileLayer.AppSource]: getAppSourcePath(this.config, dir),
};

return files;
const merge = this.canMerge();

for (const layer of getWorkspaceFileLayers()) {
await collect(directories[layer], layer);

if (!merge && files.size > 0) {
break;
}
}

return new Set(files.values());
}

protected async findFiles(directory: string): Promise<Set<EntrypointFile>> {
Expand Down
9 changes: 9 additions & 0 deletions src/cli/entrypoint/finder/AbstractEntrypointFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path";
import pluralize from "pluralize";

import AbstractOptionsFinder from "./AbstractOptionsFinder";
import {FileLayer, setFilePrecedence} from "./utils/filePrecedence";

import {getAppSourcePath, getSharedPath} from "@cli/resolvers/path";

Expand Down Expand Up @@ -33,6 +34,10 @@ export default abstract class<O extends EntrypointOptions> extends AbstractOptio

const appFiles = this.findFiles(getAppSourcePath(this.config));

for (const file of appFiles) {
setFilePrecedence(file, {layer: FileLayer.AppSource});
}

if (appFiles.size > 0) {
files = appFiles;

Expand All @@ -44,6 +49,10 @@ export default abstract class<O extends EntrypointOptions> extends AbstractOptio
if ((appFiles.size > 0 && this.canMerge()) || appFiles.size === 0) {
const sharedFiles = this.findFiles(getSharedPath(this.config));

for (const file of sharedFiles) {
setFilePrecedence(file, {layer: FileLayer.Shared});
}

if (sharedFiles.size > 0) {
files = new Set([...files, ...sharedFiles]);

Expand Down
37 changes: 7 additions & 30 deletions src/cli/entrypoint/finder/AbstractFinder.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import _ from "lodash";
import path from "path";
import fs from "fs";
import {createRequire} from "module";
import {fileURLToPath} from "url";

import {compareFilePrecedence} from "./utils/filePrecedence";

import {toPosixPath} from "@cli/utils/path";
import {isFile} from "@cli/utils/fs";
import {
getAppPath,
getAppSourcePath,
getResolvePath,
getSharedPath,
getSourcePath,
resolveRootPath,
} from "@cli/resolvers/path";
import {getResolvePath, getSourcePath, resolveRootPath} from "@cli/resolvers/path";
import {resolveAssetsPath, resolveEntrypointPath} from "@cli/entrypoint/utils";

import {ReadonlyConfig} from "@typing/config";
Expand All @@ -24,19 +18,9 @@ export default abstract class implements EntrypointFinder {

private readonly require = createRequire(import.meta.url);

protected readonly priorityDirectories: string[];

protected abstract getFiles(): Promise<Set<EntrypointFile>>;

protected constructor(protected readonly config: ReadonlyConfig) {
this.priorityDirectories = [
"node_modules",
getSourcePath(config),
getSharedPath(config),
getAppPath(config),
getAppSourcePath(config),
];
}
protected constructor(protected readonly config: ReadonlyConfig) {}

public clear(): this {
this._files = undefined;
Expand All @@ -50,11 +34,10 @@ export default abstract class implements EntrypointFinder {
}

const files = Array.from(await this.getFiles()).sort((a, b) => {
const priorityA = this.priority(a);
const priorityB = this.priority(b);
const precedence = compareFilePrecedence(a, b);

if (priorityA !== priorityB) {
return priorityA - priorityB;
if (precedence !== 0) {
return precedence;
}

return this.sortKey(a).localeCompare(this.sortKey(b));
Expand Down Expand Up @@ -133,12 +116,6 @@ export default abstract class implements EntrypointFinder {
}
}

protected priority(file: EntrypointFile): number {
const priority = _.findIndex(this.priorityDirectories, dir => file.file.includes(dir));

return priority >= 0 ? priority : this.priorityDirectories.length;
}

protected sortKey(file: EntrypointFile): string {
return toPosixPath(file.import || file.file);
}
Expand Down
8 changes: 3 additions & 5 deletions src/cli/entrypoint/finder/AbstractViewFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,16 @@ export default abstract class<O extends ViewEntrypointOptions> extends AbstractP

protected async getViews(): Promise<ViewItems<O>> {
const views: ViewItems<O> = new Map();
const entries = [...(await this.plugin().options())];
const candidates = this.allowMultiple() ? entries : entries.slice(-1);

for (const [file, options] of await this.plugin().options()) {
for (const [file, options] of candidates) {
views.set(this.createViewName(file, options), {
alias: this.createViewAlias(file, options),
filename: this.createViewFilename(file, options),
file,
options,
});

if (!this.allowMultiple()) {
break;
}
}

return views;
Expand Down
22 changes: 21 additions & 1 deletion src/cli/entrypoint/finder/AssetPluginFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {processPluginHandler} from "@cli/resolvers/plugin";

import AbstractFinder from "./AbstractFinder";
import AbstractAssetFinder from "./AbstractAssetFinder";
import {FileLayer, FileSpecificity, setFilePrecedence, setFileSpecificity} from "./utils/filePrecedence";

import {EntrypointFile} from "@typing/entrypoint";
import {ReadonlyConfig} from "@typing/config";
Expand All @@ -27,9 +28,14 @@ export default class extends AbstractFinder {
})
);

// Plugin files preserve registration identity, order and sequence.
// Canonical-path deduplication is intentionally limited to overlapping
// workspace paths in AbstractAssetFinder.
const files = new Set<EntrypointFile>();

for await (let {name, result} of pluginResult) {
for (let pluginIndex = 0; pluginIndex < pluginResult.length; pluginIndex++) {
let {name, result} = pluginResult[pluginIndex];

if (_.isBoolean(result)) {
result = pluralize(this.key);
}
Expand All @@ -39,6 +45,8 @@ export default class extends AbstractFinder {
}

if (_.isArray(result) || _.isSet(result)) {
let sequence = 0;

for (const item of result) {
if (_.isEmpty(item)) {
continue;
Expand All @@ -63,7 +71,19 @@ export default class extends AbstractFinder {
const {name: filename} = path.parse(file.file);

if (filename.endsWith(`.${this.config.browser}`) || !filename.includes(".")) {
setFilePrecedence(file, {
layer: FileLayer.Plugin,
order: pluginIndex,
sequence,
});
setFileSpecificity(
file,
filename.endsWith(`.${this.config.browser}`)
? FileSpecificity.Browser
: FileSpecificity.Generic
);
files.add(file);
sequence++;
}
}
}
Expand Down
Loading
Loading