blob: 5f1d58acba03f90ffaea93031ea44b70a02ae6f1 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
const DEVELOPMENT_DIRECTORIES = new Set([
'.nyc_output',
'__fixtures__',
'__tests__',
'coverage',
'fixture',
'fixtures',
'test',
'tests',
]);
const LOCAL_PACKAGE_PREFIX = '@maka/';
const ASF_WIP_DISCLAIMER_MARKER = '<!-- ASF-WIP-DISCLAIMER -->';
function manifestFromEntry(entry) {
return entry?.manifest ?? entry;
}
export function renderNpmReadme(readme, disclaimer) {
const markerCount = readme.split(ASF_WIP_DISCLAIMER_MARKER).length - 1;
if (markerCount !== 1) {
throw new Error(`CLI README must contain exactly one ${ASF_WIP_DISCLAIMER_MARKER} marker.`);
}
if (!disclaimer.trim()) throw new Error('DISCLAIMER-WIP must not be empty.');
return readme.replace(ASF_WIP_DISCLAIMER_MARKER, disclaimer.trimEnd());
}
export function collectWorkspaceDependencyClosure(entryName, manifestsByName) {
const closure = [];
const complete = new Set();
const visiting = new Set();
function visit(packageName) {
if (complete.has(packageName)) return;
if (visiting.has(packageName)) {
throw new Error(`Workspace dependency cycle reached ${packageName}.`);
}
const entry = manifestsByName.get(packageName);
if (!entry) throw new Error(`Workspace package ${packageName} is missing.`);
visiting.add(packageName);
const manifest = manifestFromEntry(entry);
for (const dependencyName of Object.keys(manifest.dependencies ?? {}).sort()) {
if (manifestsByName.has(dependencyName)) visit(dependencyName);
else if (dependencyName.startsWith(LOCAL_PACKAGE_PREFIX)) {
throw new Error(
`${packageName} depends on local package ${dependencyName}, but it is not in workspaces.`,
);
}
}
visiting.delete(packageName);
complete.add(packageName);
closure.push(packageName);
}
visit(entryName);
return closure;
}
export function orderWorkspaceBuilds(workspaces) {
const workspacesByName = new Map(workspaces.map((workspace) => [workspace.name, workspace]));
const order = [];
const complete = new Set();
const visiting = new Set();
function visit(packageName) {
if (complete.has(packageName)) return;
if (visiting.has(packageName)) {
throw new Error(`Workspace build dependency cycle reached ${packageName}.`);
}
const workspace = workspacesByName.get(packageName);
if (!workspace) throw new Error(`Selected workspace package ${packageName} is missing.`);
visiting.add(packageName);
const manifest = manifestFromEntry(workspace);
const dependencies = {
...manifest.dependencies,
...manifest.devDependencies,
};
for (const dependencyName of Object.keys(dependencies).sort()) {
if (workspacesByName.has(dependencyName)) visit(dependencyName);
}
visiting.delete(packageName);
complete.add(packageName);
order.push(packageName);
}
for (const { name } of workspaces) visit(name);
return order;
}
export function workspaceReleaseFiles(manifest) {
const declared = Object.hasOwn(manifest, 'releaseFiles') ? manifest.releaseFiles : ['dist'];
if (!Array.isArray(declared) || declared.length === 0) {
throw new Error(`${manifest.name ?? 'Workspace package'} releaseFiles must be non-empty.`);
}
const releaseFiles = declared.map((path) => {
if (typeof path !== 'string' || path.length === 0) {
throw new Error(`${manifest.name ?? 'Workspace package'} releaseFiles must be paths.`);
}
if (
isAbsolute(path) ||
path.includes('\\') ||
/[*?[\]{}]/u.test(path) ||
path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')
) {
throw new Error(`${manifest.name ?? 'Workspace package'} has unsafe release file ${path}.`);
}
return path;
});
if (new Set(releaseFiles).size !== releaseFiles.length) {
throw new Error(`${manifest.name ?? 'Workspace package'} releaseFiles contain duplicates.`);
}
for (const [index, path] of releaseFiles.entries()) {
const overlap = releaseFiles
.slice(index + 1)
.find((candidate) => path.startsWith(`${candidate}/`) || candidate.startsWith(`${path}/`));
if (overlap) {
throw new Error(
`${manifest.name ?? 'Workspace package'} releaseFiles overlap at ${path} and ${overlap}.`,
);
}
}
if (!releaseFiles.includes('dist')) {
throw new Error(`${manifest.name ?? 'Workspace package'} releaseFiles must include dist.`);
}
return releaseFiles;
}
export function resolveWorkspaceReleaseFiles(directory, manifest) {
const releaseFiles = workspaceReleaseFiles(manifest);
for (const releaseFile of releaseFiles) {
const entry = lstatSync(join(directory, ...releaseFile.split('/')), { throwIfNoEntry: false });
if (!entry) {
throw new Error(
`${manifest.name ?? 'Workspace package'} release file is missing: ${releaseFile}`,
);
}
if (releaseFile === 'dist') {
if (!entry.isDirectory()) {
throw new Error(
`${manifest.name ?? 'Workspace package'} release dist must be a directory.`,
);
}
} else if (!entry.isFile()) {
throw new Error(
`${manifest.name ?? 'Workspace package'} release asset must be a regular file: ${releaseFile}`,
);
}
}
return releaseFiles;
}
export function resolveReleaseWorkspacePackages(repoRoot, entryName = 'maka-agent') {
const resolvedRepoRoot = realpathSync(repoRoot);
const rootManifest = JSON.parse(readFileSync(join(resolvedRepoRoot, 'package.json'), 'utf8'));
const manifestsByName = new Map();
for (const workspacePath of rootManifest.workspaces ?? []) {
if (typeof workspacePath !== 'string' || /[*?[\]{}]/u.test(workspacePath)) {
throw new Error(`CLI release requires explicit workspace paths, found ${workspacePath}.`);
}
const directory = realpathSync(resolve(resolvedRepoRoot, workspacePath));
const pathFromRepo = relative(resolvedRepoRoot, directory);
if (pathFromRepo === '..' || pathFromRepo.startsWith(`..${sep}`) || isAbsolute(pathFromRepo)) {
throw new Error(`Workspace path escapes the repository: ${workspacePath}`);
}
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
if (typeof manifest.name !== 'string' || !manifest.name) {
throw new Error(`${workspacePath}/package.json is missing a package name.`);
}
if (manifestsByName.has(manifest.name)) {
throw new Error(`Duplicate workspace package name ${manifest.name}.`);
}
manifestsByName.set(manifest.name, { directory, manifest, workspacePath });
}
return collectWorkspaceDependencyClosure(entryName, manifestsByName).map((name) => ({
name,
...manifestsByName.get(name),
}));
}
export function releaseNpmEnvironment(environment, userConfigPath) {
return {
...Object.fromEntries(
Object.entries(environment).filter(
([name]) =>
name.toLowerCase() !== 'npm_config_allow_scripts' &&
name.toLowerCase() !== 'npm_config_userconfig',
),
),
npm_config_userconfig: userConfigPath,
};
}
export function isThirdPartyDevelopmentArtifact(relativePath) {
const segments = relativePath.split(/[\\/]/).filter(Boolean);
if (segments.some((segment) => DEVELOPMENT_DIRECTORIES.has(segment))) return true;
const file = segments.at(-1) ?? '';
return (
/\.(?:spec|test)\.(?:cjs|js|mjs)$/.test(file) ||
/\.d\.ts(?:\.map)?$/.test(file) ||
/\.(?:cjs|js|mjs)\.map$/.test(file) ||
/\.(?:cts|mts|ts|tsx)$/.test(file) ||
file.endsWith('.tsbuildinfo')
);
}
// Modules that only a test or E2E entry point may import. They are a
// Maka-owned convention, so they are not part of DEVELOPMENT_DIRECTORIES: a
// third-party package is free to ship a directory by that name.
const MAKA_TEST_ONLY_DIRECTORY = 'test-only';
export function isMakaDevelopmentArtifact(relativePath) {
const segments = relativePath.split(/[\\/]/).filter(Boolean);
if (segments.some((segment) => segment === 'src')) return true;
if (segments.some((segment) => segment === MAKA_TEST_ONLY_DIRECTORY)) return true;
if (segments.some((segment) => DEVELOPMENT_DIRECTORIES.has(segment))) return true;
const file = segments.at(-1) ?? '';
return (
file === 'dev-cli.js' ||
/\.(?:spec|test)\.js$/.test(file) ||
/\.d\.ts(?:\.map)?$/.test(file) ||
/\.js\.map$/.test(file)
);
}
export function isCurrentDevelopmentJavaScript(
workspaceRoot,
relativePath,
generatedFiles = new Set(),
) {
const portablePath = relativePath.split(/[\\/]/u).join('/');
if (generatedFiles.has(portablePath)) return true;
if (!portablePath.endsWith('.js')) return false;
const sourcePath = portablePath.slice(0, -'.js'.length);
return ['.ts', '.tsx', '.mts', '.cts'].some((extension) =>
existsSync(join(workspaceRoot, 'src', `${sourcePath}${extension}`)),
);
}