Skip to content
This repository was archived by the owner on Nov 18, 2025. It is now read-only.

Commit 0fb1cf9

Browse files
authored
feat: add ESM tools in gax (#1459)
This PR does the following: 1. Adds 3 babel plugins to help transform ESM code to CJS: one to transform `path.dirname(fileURLToPath(import.meta)` to `__dirname`, another to turn `isEsm` to false, and lastly a final one to turn proxyquire into esmock. 3. Adds an ESM-path in compileProtos (searches for source code one level deeper, and generates `protos.js` and `protos.cjs` in es6 and amd, respectively)
1 parent 6052c9a commit 0fb1cf9

15 files changed

+3878
-9
lines changed

tools/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@
3232
"google-proto-files": "^4.0.0",
3333
"protobufjs-cli": "1.1.2",
3434
"rimraf": "^5.0.1",
35+
"@babel/core": "^7.22.5",
36+
"@babel/traverse": "^7.22.5",
3537
"uglify-js": "^3.17.0",
3638
"walkdir": "^0.4.0"
3739
},
3840
"repository": "googleapis/gax-nodejs",
3941
"devDependencies": {
42+
"@babel/cli": "^7.22.5",
43+
"@babel/types": "^7.22.5",
44+
"@types/babel__core": "^7.20.1",
45+
"@types/babel__traverse": "^7.20.1",
4046
"@types/mocha": "^9.0.0",
4147
"@types/ncp": "^2.0.1",
4248
"@types/uglify-js": "^3.17.0",

tools/src/compileProtos.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ function updateDtsTypes(dts: string, enums: Set<string>): string {
163163
}
164164

165165
function fixJsFile(js: string): string {
166+
// 0. fix protobufjs import: we don't want the libraries to
167+
// depend on protobufjs, so we re-export it from google-gax
168+
js = js.replace(
169+
'import * as $protobuf from "protobufjs/minimal"',
170+
'import {protobufMinimal as $protobuf} from "google-gax/build/src/protobuf.js"'
171+
);
172+
166173
// 1. fix protobufjs require: we don't want the libraries to
167174
// depend on protobufjs, so we re-export it from google-gax
168175
js = js.replace(
@@ -211,13 +218,19 @@ function fixDtsFile(dts: string): string {
211218
* @param {string[]} protoJsonFiles List of JSON files to parse
212219
* @return {Promise<string[]>} Resolves to an array of proto files.
213220
*/
214-
async function buildListOfProtos(protoJsonFiles: string[]): Promise<string[]> {
221+
async function buildListOfProtos(
222+
protoJsonFiles: string[],
223+
esm?: boolean
224+
): Promise<string[]> {
215225
const result: string[] = [];
216226
for (const file of protoJsonFiles) {
217227
const directory = path.dirname(file);
218228
const content = await readFile(file);
219229
const list = JSON.parse(content.toString()).map((filePath: string) =>
220-
path.join(directory, normalizePath(filePath))
230+
// If we're in ESM, we're going to be in a directory level below normal
231+
esm
232+
? path.join(directory, '..', normalizePath(filePath))
233+
: path.join(directory, normalizePath(filePath))
221234
);
222235
result.push(...list);
223236
}
@@ -236,7 +249,8 @@ async function buildListOfProtos(protoJsonFiles: string[]): Promise<string[]> {
236249
async function compileProtos(
237250
rootName: string,
238251
protos: string[],
239-
skipJson = false
252+
skipJson = false,
253+
esm = false
240254
): Promise<void> {
241255
if (!skipJson) {
242256
// generate protos.json file from proto list
@@ -261,7 +275,9 @@ async function compileProtos(
261275
}
262276

263277
// generate protos/protos.js from protos.json
264-
const jsOutput = path.join('protos', 'protos.js');
278+
const jsOutput = esm
279+
? path.join('protos', 'protos.cjs')
280+
: path.join('protos', 'protos.js');
265281
const pbjsArgs4js = [
266282
'-r',
267283
rootName,
@@ -281,9 +297,34 @@ async function compileProtos(
281297
jsResult = fixJsFile(jsResult);
282298
await writeFile(jsOutput, jsResult);
283299

300+
let jsOutputEsm;
301+
if (esm) {
302+
jsOutputEsm = path.join('protos', 'protos.js');
303+
const pbjsArgs4jsEsm = [
304+
'-r',
305+
rootName,
306+
'--target',
307+
'static-module',
308+
'-p',
309+
'protos',
310+
'-p',
311+
gaxProtos,
312+
'-o',
313+
jsOutputEsm,
314+
'-w',
315+
'es6',
316+
];
317+
pbjsArgs4jsEsm.push(...protos);
318+
await pbjsMain(pbjsArgs4jsEsm);
319+
320+
let jsResult = (await readFile(jsOutputEsm)).toString();
321+
jsResult = fixJsFile(jsResult);
322+
await writeFile(jsOutputEsm, jsResult);
323+
}
324+
284325
// generate protos/protos.d.ts
285326
const tsOutput = path.join('protos', 'protos.d.ts');
286-
const pbjsArgs4ts = [jsOutput, '-o', tsOutput];
327+
const pbjsArgs4ts = [esm ? jsOutputEsm! : jsOutput, '-o', tsOutput];
287328
await pbtsMain(pbjsArgs4ts);
288329

289330
let tsResult = (await readFile(tsOutput)).toString();
@@ -330,27 +371,38 @@ export async function generateRootName(directories: string[]): Promise<string> {
330371
export async function main(parameters: string[]): Promise<void> {
331372
const protoJsonFiles: string[] = [];
332373
let skipJson = false;
374+
let esm = false;
333375
const directories: string[] = [];
334376
for (const parameter of parameters) {
335377
if (parameter === '--skip-json') {
336378
skipJson = true;
337379
continue;
338380
}
381+
if (parameter === '--esm') {
382+
esm = true;
383+
continue;
384+
}
339385
// it's not an option so it's a directory
340386
const directory = parameter;
341387
directories.push(directory);
342388
protoJsonFiles.push(...(await findProtoJsonFiles(directory)));
343389
}
344390
const rootName = await generateRootName(directories);
345-
const protos = await buildListOfProtos(protoJsonFiles);
346-
await compileProtos(rootName, protos, skipJson);
391+
if (esm) {
392+
const esmProtos = await buildListOfProtos(protoJsonFiles, esm);
393+
await compileProtos(rootName, esmProtos, skipJson, esm);
394+
}
395+
const protos = await buildListOfProtos(protoJsonFiles, esm);
396+
await compileProtos(rootName, protos, skipJson, esm);
347397
}
348398

349399
/**
350400
* Shows the usage information.
351401
*/
352402
function usage() {
353-
console.log(`Usage: node ${process.argv[1]} [--skip-json] directory ...`);
403+
console.log(
404+
`Usage: node ${process.argv[1]} [--skip-json] [--esm] directory ...`
405+
);
354406
console.log(
355407
`Finds all files matching ${PROTO_LIST_REGEX} in the given directories.`
356408
);

tools/src/replaceESMMockingLib.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Visitor, types} from '@babel/core';
16+
17+
export interface PluginOptions {
18+
opts?: {
19+
fromLibName?: string;
20+
toLibName?: string;
21+
};
22+
}
23+
24+
export default function replaceESMMockingLib(): {
25+
visitor: Visitor<PluginOptions>;
26+
} {
27+
return {
28+
visitor: {
29+
ImportDeclaration(path, state) {
30+
const opts = state.opts || {};
31+
const fromLib = opts.fromLibName || 'esmock';
32+
const toLib = opts.toLibName || 'proxyquire';
33+
const {node} = path;
34+
35+
node.specifiers.forEach(spec => {
36+
if (spec.local.name !== fromLib) {
37+
return;
38+
}
39+
spec.local.name = spec.local.name.replace(fromLib, toLib);
40+
});
41+
42+
if (node.source.value !== fromLib) {
43+
return;
44+
}
45+
node.source.value = node.source.value.replace(fromLib, toLib);
46+
},
47+
CallExpression(path, state) {
48+
const opts = state.opts || {};
49+
const fromLib = opts.fromLibName || 'esmock';
50+
const toLib = opts.toLibName || 'proxyquire';
51+
const {node} = path;
52+
53+
if (types.isIdentifier(node.callee)) {
54+
if (node.callee.name !== fromLib) {
55+
return;
56+
}
57+
58+
node.callee.name = toLib;
59+
path.parentPath.replaceWith(types.expressionStatement(node));
60+
}
61+
},
62+
},
63+
};
64+
}

tools/src/replaceImportMetaUrl.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// eslint-disable-next-line node/no-extraneous-import
16+
import {smart} from '@babel/template';
17+
import {Statement} from '@babel/types';
18+
import {Visitor} from '@babel/core';
19+
20+
export interface PluginOptions {
21+
opts?: {
22+
replacementValue?: string;
23+
};
24+
}
25+
26+
export default function replaceImportMetaUrl(): {
27+
visitor: Visitor<PluginOptions>;
28+
} {
29+
return {
30+
visitor: {
31+
CallExpression(path, state) {
32+
const opts = state.opts || {};
33+
const replacementValue = opts.replacementValue || '__dirname';
34+
const {node} = path;
35+
if (
36+
node.callee.type === 'MemberExpression' &&
37+
node.callee.property.type === 'Identifier' &&
38+
node.callee.property.name === 'dirname' &&
39+
node.arguments[0].type === 'CallExpression' &&
40+
node.arguments[0].callee.type === 'Identifier' &&
41+
node.arguments[0].callee.name === 'fileURLToPath' &&
42+
node.arguments[0].arguments[0].type === 'MemberExpression' &&
43+
node.arguments[0].arguments[0].object.type === 'MetaProperty' &&
44+
node.arguments[0].arguments[0].object.meta.type === 'Identifier' &&
45+
node.arguments[0].arguments[0].object.meta.name === 'import' &&
46+
node.arguments[0].arguments[0].object.property.type ===
47+
'Identifier' &&
48+
node.arguments[0].arguments[0].object.property.name === 'meta' &&
49+
node.arguments[0].arguments[0].property.type === 'Identifier' &&
50+
node.arguments[0].arguments[0].property.name === 'url'
51+
) {
52+
const replacement = smart.ast`${replacementValue}` as Statement;
53+
path.replaceWith(replacement);
54+
}
55+
},
56+
},
57+
};
58+
}

tools/src/toggleESMFlagVariable.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Visitor, types} from '@babel/core';
16+
17+
export interface PluginOptions {
18+
opts?: {
19+
variableIdentifier?: string;
20+
replacementValue?: boolean;
21+
};
22+
}
23+
24+
export default function toggleESMFlagVariable(): {
25+
visitor: Visitor<PluginOptions>;
26+
} {
27+
return {
28+
visitor: {
29+
VariableDeclarator(path, state) {
30+
const opts = state.opts || {};
31+
const variableIdentifier = opts.variableIdentifier || 'isEsm';
32+
const replacementValue = opts.replacementValue || false;
33+
const {node} = path;
34+
const identifier = node.id as types.Identifier;
35+
if (
36+
identifier.name === variableIdentifier &&
37+
node.init?.type === 'BooleanLiteral'
38+
) {
39+
node.init.value = replacementValue;
40+
}
41+
},
42+
},
43+
};
44+
}

0 commit comments

Comments
 (0)