scripts/generate-clients/copy-to-clients.js (194 lines of code) (raw):
// @ts-check
const { join } = require("path");
const { copySync, removeSync } = require("fs-extra");
const prettier = require("prettier");
const semver = require("semver");
const { readdirSync, lstatSync, readFileSync, existsSync, writeFileSync } = require("fs");
const getOverwritableDirectories = (subDirectories, packageName) => {
const additionalOverwritablePaths = {
"@aws-sdk/client-sts": ["defaultRoleAssumers.ts", "defaultStsRoleAssumers.ts", "defaultRoleAssumers.spec.ts"],
};
const unoverwritablePaths = {
"@aws-sdk/client-transcribe-streaming": ["README.md"],
};
const overwritablePaths = [
"src", // contains all source files
"LICENCE",
"README.md",
];
return subDirectories.filter((path) => {
const isUnoverwritablePaths = unoverwritablePaths[packageName]?.indexOf(path) >= 0;
const isProtocolTestFolder = packageName.startsWith("@aws-sdk/aws-") && path === "test";
const isOverwritableDirectory = overwritablePaths.indexOf(path) >= 0;
const isAdditionalOverwritablePaths = additionalOverwritablePaths[packageName]?.indexOf(path) >= 0;
return !isUnoverwritablePaths && (isOverwritableDirectory || isProtocolTestFolder || isAdditionalOverwritablePaths);
});
};
/**
* Copy the keys from newly-generated package.json to
* existing package.json. For each keys in new package.json
* we prefer the new key. Whereas for the values, we prefer
* the values in the existing package.json.
*
* This behavior enables us removing dependencies/scripts
* from codegen, but maintain the newer dependency versions
* in existing package.json
*/
const mergeManifest = (fromContent = {}, toContent = {}, parentKey = "root") => {
const merged = {};
for (const name of Object.keys(fromContent)) {
if (fromContent[name].constructor.name === "Object") {
if (name === "devDependencies") {
// Use same versions of devDependencies across all workspaces.
// After moving to yarn modern, we'll use constraints feature to enforce
// consistency in dependency versions https://yarnpkg.com/features/constraints
const devDepToVersionHash = {
"@tsconfig/node18": "18.2.4",
concurrently: "7.0.0",
"downlevel-dts": "0.10.1",
rimraf: "3.0.2",
typescript: "~5.2.2",
};
fromContent[name] = Object.keys(fromContent[name])
.filter((dep) => Object.keys(devDepToVersionHash).includes(dep))
.reduce((acc, dep) => ({ ...acc, [dep]: devDepToVersionHash[dep] }), fromContent[name]);
// ToDo: Remove if typedoc is config driven or removed in smithy-typescript.
delete fromContent[name]["typedoc"];
}
if (name === "scripts" && !fromContent[name]["build:include:deps"]) {
fromContent[name]["build:include:deps"] = "lerna run --scope $npm_package_name --include-dependencies build";
}
merged[name] = mergeManifest(fromContent[name], toContent[name], name);
if (name === "scripts" || name === "devDependencies") {
// Allow target package.json(toContent) has its own special script or
// dev dependencies that won't be overwritten in codegen
merged[name] = { ...toContent[name], ...merged[name] };
}
if (name === "scripts" || name === "dependencies" || name === "devDependencies") {
// Sort by keys to make sure the order is stable
merged[name] = Object.fromEntries(Object.entries(merged[name]).sort());
}
} else if (name.indexOf("@aws-sdk/") === 0) {
// If it's internal dependency, use current version in the repo if not
// present in package.json
merged[name] = toContent[name] || "*";
} else {
// If key (say dependency) is present in both codegen and
// package.json, we prefer latter
merged[name] = toContent[name] || fromContent[name];
// use the higher version dependency.
if (parentKey === "dependencies" || parentKey === "devDependencies") {
const toSemver = semver.coerce(toContent[name]);
const fromSemver = semver.coerce(fromContent[name]);
if (semver.valid(toSemver) && semver.valid(fromSemver)) {
const useToContentVersion = semver.gt(toSemver, fromSemver);
if (useToContentVersion) {
merged[name] = toContent[name];
} else {
merged[name] = fromContent[name];
}
} else {
if (toContent[name] === "*" && fromContent[name] !== "*") {
merged[name] = fromContent[name];
}
}
}
}
}
return merged;
};
const copyToClients = async (sourceDir, destinationDir, solo) => {
for (const modelName of readdirSync(sourceDir)) {
if (modelName === "source") continue;
const artifactPath = join(sourceDir, modelName, "typescript-codegen");
const packageManifestPath = join(artifactPath, "package.json");
if (!existsSync(packageManifestPath)) {
console.error(`${modelName} generates empty client, skip.`);
continue;
}
const packageManifest = JSON.parse(readFileSync(packageManifestPath).toString());
const packageName = packageManifest.name;
const clientName = packageName.replace("@aws-sdk/", "");
if (solo && clientName !== `client-${solo}`) {
continue;
}
console.log(`copying ${packageName} from ${artifactPath} to ${destinationDir}`);
const destPath = join(destinationDir, clientName);
// Code to move files/folders prefixed with `doc-client-` to `lib/lib-dynamodb`
if (clientName === "client-dynamodb") {
for (const packageSub of readdirSync(artifactPath)) {
if (packageSub.startsWith("doc-client-")) {
const destinationFileName = packageSub.replace("doc-client-", "");
const docClientArtifactPath = join(artifactPath, packageSub);
const docClientDestPath = join(destinationDir, "..", "lib", "lib-dynamodb", "src", destinationFileName);
copySync(docClientArtifactPath, docClientDestPath, { overwrite: true });
removeSync(docClientArtifactPath);
}
}
}
const packageSubs = readdirSync(artifactPath);
const overWritableSubs = getOverwritableDirectories(packageSubs, packageName);
for (const packageSub of packageSubs) {
const packageSubPath = join(artifactPath, packageSub);
const destSubPath = join(destPath, packageSub);
if (packageSub === "package.json") {
//copy manifest file
const destManifest = existsSync(destSubPath) ? JSON.parse(readFileSync(destSubPath).toString()) : {};
const mergedManifest = {
...mergeManifest(packageManifest, destManifest),
homepage: `https://github.com/aws/aws-sdk-js-v3/tree/main/clients/${clientName}`,
repository: {
type: "git",
url: "https://github.com/aws/aws-sdk-js-v3.git",
directory: `clients/${clientName}`,
},
};
// no need for the default prepack script
delete mergedManifest.scripts.prepack;
// ToDo: Remove if typedoc is config driven or removed in smithy-typescript.
delete mergedManifest.scripts["build:docs"];
if (!mergedManifest.private) {
mergedManifest.scripts["extract:docs"] = "api-extractor run --local";
}
const serviceName = clientName.replace("client-", "");
const modelFile = join(__dirname, "..", "..", "codegen", "sdk-codegen", "aws-models", serviceName + ".json");
if (existsSync(modelFile)) {
mergedManifest.scripts[
"generate:client"
] = `node ../../scripts/generate-clients/single-service --solo ${serviceName}`;
}
writeFileSync(destSubPath, prettier.format(JSON.stringify(mergedManifest), { parser: "json-stringify" }));
} else if (packageSub === "typedoc.json") {
// Skip writing typedoc.json
// ToDo: Remove if typedoc.json is config driven or removed in smithy-typescript.
} else if (overWritableSubs.includes(packageSub) || !existsSync(destSubPath)) {
if (lstatSync(packageSubPath).isDirectory()) removeSync(destSubPath);
copySync(packageSubPath, destSubPath, {
overwrite: true,
});
}
}
}
};
const copyServerTests = async (sourceDir, destinationDir) => {
for (const modelName of readdirSync(sourceDir)) {
if (modelName === "source") continue;
const artifactPath = join(sourceDir, modelName, "typescript-ssdk-codegen");
const packageManifestPath = join(artifactPath, "package.json");
if (!existsSync(packageManifestPath)) {
console.error(`${modelName} generates empty server, skip.`);
continue;
}
const packageManifest = JSON.parse(readFileSync(packageManifestPath).toString());
const packageName = packageManifest.name;
const testName = packageName.replace("@aws-sdk/", "");
console.log(`copying ${packageName} from ${artifactPath} to ${destinationDir}`);
const destPath = join(destinationDir, testName);
const packageSubs = readdirSync(artifactPath);
const overWritableSubs = getOverwritableDirectories(packageSubs, packageName);
for (const packageSub of packageSubs) {
const packageSubPath = join(artifactPath, packageSub);
const destSubPath = join(destPath, packageSub);
if (packageSub === "package.json") {
//copy manifest file
const destManifest = existsSync(destSubPath) ? JSON.parse(readFileSync(destSubPath).toString()) : {};
const mergedManifest = {
...mergeManifest(packageManifest, destManifest),
homepage: `https://github.com/aws/aws-sdk-js-v3/tree/main/private/${testName}`,
repository: {
type: "git",
url: "https://github.com/aws/aws-sdk-js-v3.git",
directory: `private/${testName}`,
},
};
if (!mergedManifest.scripts.test) {
mergedManifest.scripts.test = "jest";
}
if (mergedManifest.private) {
// don't generate documentation for private packages
delete mergedManifest.scripts["build:docs"];
}
writeFileSync(destSubPath, prettier.format(JSON.stringify(mergedManifest), { parser: "json-stringify" }));
} else if (packageSub === "typedoc.json") {
// Skip writing typedoc.json
// ToDo: Remove if typedoc.json is config driven or removed in smithy-typescript.
} else if (overWritableSubs.includes(packageSub) || !existsSync(destSubPath)) {
if (lstatSync(packageSubPath).isDirectory()) removeSync(destSubPath);
copySync(packageSubPath, destSubPath, {
overwrite: true,
});
}
}
}
};
module.exports = {
copyToClients,
copyServerTests,
};