Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
310 lines
11 KiB
JavaScript
310 lines
11 KiB
JavaScript
/*
|
|
* 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() && !entry.isDirectory()) {
|
|
throw new Error(
|
|
`${manifest.name ?? 'Workspace package'} release asset must be a regular file or directory: ${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)
|
|
);
|
|
}
|
|
|
|
function releaseExportTarget(target) {
|
|
if (typeof target === 'string') {
|
|
return isMakaDevelopmentArtifact(target) ? undefined : target;
|
|
}
|
|
if (Array.isArray(target)) {
|
|
const projected = target.map(releaseExportTarget).filter((entry) => entry !== undefined);
|
|
return projected.length === 0 ? undefined : projected;
|
|
}
|
|
if (target && typeof target === 'object') {
|
|
const projected = Object.fromEntries(
|
|
Object.entries(target)
|
|
.map(([condition, value]) => [condition, releaseExportTarget(value)])
|
|
.filter(([, value]) => value !== undefined),
|
|
);
|
|
return Object.keys(projected).length === 0 ? undefined : projected;
|
|
}
|
|
return target;
|
|
}
|
|
|
|
const WORKSPACE_RELEASE_MANIFEST_FIELDS = [
|
|
'name',
|
|
'version',
|
|
'description',
|
|
'license',
|
|
'type',
|
|
'sideEffects',
|
|
'main',
|
|
'exports',
|
|
'bin',
|
|
'engines',
|
|
'dependencies',
|
|
'optionalDependencies',
|
|
'peerDependencies',
|
|
'peerDependenciesMeta',
|
|
];
|
|
|
|
export function workspaceReleaseManifest(manifest) {
|
|
const releaseManifest = Object.fromEntries(
|
|
WORKSPACE_RELEASE_MANIFEST_FIELDS.filter((field) => manifest[field] !== undefined).map(
|
|
(field) => [field, manifest[field]],
|
|
),
|
|
);
|
|
if (releaseManifest.exports !== undefined) {
|
|
const projected = releaseExportTarget(releaseManifest.exports);
|
|
releaseManifest.exports = projected === undefined ? {} : projected;
|
|
}
|
|
return releaseManifest;
|
|
}
|
|
|
|
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}`)),
|
|
);
|
|
}
|