@google-cloud/storage-control
Version:
Cloud Storage API client for Node.js
356 lines • 14.5 kB
JavaScript
;
// Copyright 2026 Google LLC
//
// Licensed 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
//
// https://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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.astHelper = astHelper;
exports.parseClientName = parseClientName;
exports.getPropertyDeclarations = getPropertyDeclarations;
const ts = require("typescript");
const EXCLUDED_FUNCTION_TERMS = [];
const AST_TYPES_TO_INCLUDE = ['MethodDeclaration', 'GetAccessor'];
let foundNodes = [];
const methodDocstrings = new Map();
let sourceFile;
/**
* This is a helper function to get the escaped text from a AST node.
* The `name` property of a MethodDeclaration or Parameter can be one of several types.
* In our use case, we know it's an Identifier, so we can safely cast it to get the
* human-readable name.
*
* @param {ts.PropertyName | ts.BindingName} name - The node to get the name from.
* @returns {string} The escaped text of the node's name.
*/
function getEscapedText(name) {
name = name;
const nameEscapedText = name.escapedText;
return nameEscapedText;
}
/**
* This is a helper function to generate the overloaded method signatures for a given method.
*
* @param {ts.MethodDeclaration | ts.GetAccessorDeclaration} node - The AST node for the method.
* @returns {string} The overloaded method signatures.
*/
function getOverloadedMethod(node) {
if (ts.isGetAccessorDeclaration(node) || !node.body) {
return '';
}
const functionName = getEscapedText(node.name);
const parameters = node.parameters;
// We are looking for standard GAPIC methods which have this signature pattern
if (parameters.length < 2 || parameters.length > 3) {
return '';
}
const requestParam = parameters[0];
const optionsOrCallbackParam = parameters[1];
// Not a standard GAPIC method if the params don't have types
if (!requestParam.type || !optionsOrCallbackParam.type || !node.type) {
return '';
}
const requestParamName = getEscapedText(requestParam.name);
const requestType = requestParam.type.getText(sourceFile);
// Extract Promise and Callback types from return type
let promiseReturnType = '';
if (ts.isUnionTypeNode(node.type)) {
const promiseTypeNode = node.type.types.find(t => t.kind === ts.SyntaxKind.TypeReference &&
t.getText(sourceFile).startsWith('Promise'));
if (promiseTypeNode) {
promiseReturnType = promiseTypeNode.getText(sourceFile);
}
}
else if (node.type.getText(sourceFile).startsWith('Promise')) {
promiseReturnType = node.type.getText(sourceFile);
}
// If there's no promise return type, it's not a method we need to overload.
if (!promiseReturnType) {
return '';
}
// Extract Callback type from the `optionsOrCallback` parameter
let callbackType = '';
if (ts.isUnionTypeNode(optionsOrCallbackParam.type)) {
const callbackTypeNode = optionsOrCallbackParam.type.types.find(t => t.kind === ts.SyntaxKind.TypeReference &&
(t.getText(sourceFile).startsWith('Callback') ||
t.getText(sourceFile).startsWith('PaginationCallback')));
if (callbackTypeNode) {
callbackType = callbackTypeNode.getText(sourceFile);
}
}
// If there's no callback type, it's not a method we need to overload.
if (!callbackType) {
return '';
}
// Make request param non-optional for callback signatures
const requiredRequestType = requestType.replace('?', '');
const promiseSignature = `\n${functionName}(${requestParamName}?: ${requestType}, options?: CallOptions): ${promiseReturnType};`;
const callbackWithOptionsSignature = `\n${functionName}(${requestParamName}: ${requiredRequestType}, options: CallOptions, callback: ${callbackType}): void;`;
const callbackWithoutOptionsSignature = `\n${functionName}(${requestParamName}: ${requiredRequestType}, callback: ${callbackType}): void;`;
return (promiseSignature +
callbackWithOptionsSignature +
callbackWithoutOptionsSignature);
}
/**
* This is a helper function to get the kind of a AST node.
*
* @param {ts.Node} node - The node to get the kind from.
* @returns {string} The kind of the node.
*/
function getKind(node) {
if (!node) {
return '';
}
return ts.SyntaxKind[node.kind];
}
/**
* This function recursively traverses the AST of a file and extracts all the
* method declarations that we want to include in our generated client.
*
* @param {ts.Node} node - The current node in the AST.
* @param {string} client - The name of the client being processed.
*/
function extract(node, client) {
const kind = getKind(node);
if (AST_TYPES_TO_INCLUDE.includes(kind)) {
if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node)) {
const nameEscapedText = getEscapedText(node.name);
if (methodDocstrings.get(nameEscapedText) === undefined) {
const commentRanges = ts.getLeadingCommentRanges(sourceFile.getFullText(), node.getFullStart());
let docString = '';
if (commentRanges) {
commentRanges.map(r => {
docString = docString.concat(sourceFile.getFullText().slice(r.pos, r.end));
if (r.hasTrailingNewLine) {
docString = docString.concat('\n');
}
});
}
methodDocstrings.set(nameEscapedText, docString);
}
if (node.body) {
foundNodes.push([node.name, node]);
}
}
}
ts.forEachChild(node, childNode => {
extract(childNode, client);
});
}
/**
* This function contains the logic to traverse each file and pull in the nodes
* from the AST that contain elements we want to use. It then generates the
* source code for the methods of our uber client.
*
* @param {string} file - The path to the file to process.
* @param {string} client - The name of the client being processed.
* @returns {string} The generated source code for the methods.
*/
function ast(file, client) {
const program = ts.createProgram([file], { allowJs: true });
sourceFile = program.getSourceFile(file);
if (!sourceFile) {
throw new Error(`Could not find source file: ${file}`);
}
// Run the extract function with the script's arguments
extract(sourceFile, client);
program.getTypeChecker();
return foundNodes.map(f => {
let output = '';
const [name, node] = f;
const escapedName = getEscapedText(name);
// create function name
const functionName = `${escapedName}`;
let isExcludedFunction = false;
for (const term of EXCLUDED_FUNCTION_TERMS) {
if (functionName.search(term) >= 0) {
isExcludedFunction = true;
break;
}
}
const isStatic = node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword);
if (!isExcludedFunction) {
const docString = methodDocstrings.get(functionName);
output = output.concat(`\n\n${docString}`);
const overloadedMethods = getOverloadedMethod(node);
output = output.concat(overloadedMethods);
if (overloadedMethods) {
output = output.concat(`\n${docString}`);
}
if (isStatic) {
output = output.concat('\tstatic ');
}
if (ts.isGetAccessorDeclaration(node)) {
output = output.concat('get ');
}
output = output.concat(`\t${functionName}(`);
// add parameters - pull in their name, whether they're optional or not, and their type
let parametersList = '';
let argumentsList = '';
if (ts.isMethodDeclaration(node)) {
for (let i = 0; i < node.parameters.length; i++) {
const name = getEscapedText(node.parameters[i].name);
const questionToken = node.parameters[i].questionToken ? '?' : '';
const typeString = node.parameters[i].type?.getFullText();
let parameter = `${name}${questionToken}: ${typeString}`;
parametersList = parametersList.concat(name);
if (name === 'optionsOrCallback') {
argumentsList = argumentsList.concat('options');
}
else {
argumentsList = argumentsList.concat(name);
}
if (i !== node.parameters.length - 1) {
parameter += ',';
parametersList += ',';
argumentsList += ',';
}
output = output.concat(`\n\t\t${parameter}`);
}
}
output = output.concat(')');
if (node.type) {
output = output.concat(`:${node.type.getFullText()}`);
}
const clientName = parseClientName(client);
// call underlying client function
if (node.body) {
let body = '';
if (ts.isGetAccessorDeclaration(node)) {
if (isStatic) {
body = `
return ${client}.${functionName};`;
}
else {
body = `
return this.${clientName}.${functionName};`;
}
}
else if (functionName === 'close') {
body = `
return Promise.all([
this.storageControlInternal.close(),
this.storageClient.close(),
]).then(() => {});
`;
}
else if (functionName === 'getProjectId') {
body = `
if (callback) {
this.${clientName}.getProjectId(callback);
return;
}
return this.${clientName}.getProjectId();
`;
}
else {
const hasRequestArg = node.parameters.some(p => getEscapedText(p.name) === 'request');
// If there's no 'request' parameter, it's a simple helper method.
if (!hasRequestArg ||
functionName.endsWith('Stream') ||
functionName.endsWith('Async')) {
body = `
return this.${clientName}.${functionName}(${argumentsList});`;
}
// Otherwise, it's a GAPIC method (streaming or standard).
else {
body = `
request = request || {};
let options: any;
if (typeof optionsOrCallback === 'function' && callback === undefined) {
callback = optionsOrCallback;
options = {};
}
else {
options = optionsOrCallback as any;
}
if (callback === undefined){
return this.${clientName}.${functionName}(request, options);
}
return this.${clientName}.${functionName}(${argumentsList});
`;
}
}
output = output.concat(`{\n${body}\n}`);
}
}
const functionKey = isStatic ? `static_${functionName}` : functionName;
return [functionKey, output];
});
}
/**
* This function loops through the files and calls the AST function on them.
*
* @param {string[]} files - A list of file paths to process.
* @param {string[]} clients - A list of client names to process.
* @returns {string} The generated source code for all the methods.
*/
function astHelper(files, clients) {
const functions = new Map();
for (const f in files) {
foundNodes = [];
const client = clients[f];
const newFunctions = ast(files[f], client);
for (const [name, source] of newFunctions) {
if (functions.has(name)) {
if (client === 'storageControlInternalClient') {
functions.set(name, source);
}
}
else {
functions.set(name, source);
}
}
}
let output = '';
functions.forEach(source => {
output += source;
});
return output;
}
/**
* This function converts client types to the names we'll use for variables.
*
* @param {string} client - The name of the client.
* @returns {string} The name of the client in camel case.
*/
function parseClientName(client) {
return client.charAt(0).toLowerCase() + client.slice(1);
}
/**
* This function returns all the public property declarations in a file.
*
* @param {string} file - The path to the file to process.
* @returns {Map<string, string>} A map of public property names to their full text.
*/
function getPropertyDeclarations(file) {
const program = ts.createProgram([file], { allowJs: true });
const sourceFile = program.getSourceFile(file);
if (!sourceFile) {
throw new Error(`Could not find source file: ${file}`);
}
const properties = new Map();
function visit(node) {
if (ts.isPropertyDeclaration(node)) {
const isPrivate = node.modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword);
if (!isPrivate) {
const propertyName = getEscapedText(node.name);
if (!propertyName.startsWith('_') && !propertyName.startsWith('#')) {
properties.set(propertyName, node.getFullText(sourceFile));
}
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return properties;
}
//# sourceMappingURL=ast_utils.js.map