Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore!: publish gax-nodejs in dual format (CJS and ESM) #1679

Open
wants to merge 59 commits into
base: main
Choose a base branch
from

Conversation

sofisl
Copy link
Contributor

@sofisl sofisl commented Dec 5, 2024

This is a massive PR... If I could make it any smaller, believe me, I would!

Changes for Node 18:
Some ESM syntax we're using requires Node 18 (for example, using with assertions to import JSON). So, this PR also includes the bare minimum needed to migrate gax to Node 18:

  • Updates the post-processor to the latest version to use Node 18 images.
  • Sync-repo-settings makes Node 18 & 20 required tests.
  • All wrokflows and testing and releasing images using Node 18 now.

Changes for ESM:

  • Added an esm/ directory-level so that this can be renamed to cjs in the build phase. This will allow modules to be more clearly defined in terms of whether a file is esm or cjs.
  • Added a .babelrc configuration file to help use custom Babel plugins to transpile ESM to CJS (as per Publishing Nodejs libraries in ESM and CJS)
  • Migrated .jsdoc.js to .json since jsdoc doesn't look for a .cjs extension, but does understand JSON
  • Other .js configuration files renamed to .cjs (mocharc.js & prettierrc.js) to be imported as common js
  • custom files:
    • add-cjs-extension.cjs: finds files using proxyquire and makes sure they import the .cjs counterpart, not esm
    • replace-protobuf-imports.cjs: replaces a line in compiled protos to import using default module vs. start notation.
  • Generally: added full file paths to all imports (i.e., import {CallSettings} from './gax' --> import {CallSettings} from './gax.js'
  • Import JSON using with {type: 'json'}; extension (this is a weak spot for ESM; as of Node 18 you can import using with assertion)
  • Changed references to __dirname to const dirname = path.dirname(fileURLToPath(import.meta.url)); (see Publishing Nodejs libraries in ESM and CJS) - we made a custom babel plugin to also transpile this to __dirname for cjs.
  • Importing protos/ is now one directory level above what it used to be as are all the root-level files (like package.json) given the new directory structure of esm/src/.... References to these files have added a directory level.
  • reran proto compilation commands (in package.json) for the common protos that gax compiles
  • Changed the webpack configuration for the browser test to properly null-load the new imported file
  • Added tsconfig.esm.json to compile into esm as well as cjs.
  • Migrated us to using node-fetch v3, which is an ESM-module! We dynamically import node-fetch.

Tests

  • Added tasks as well as speech for system tests connecting to our current libraries. This is because tasks is published in dual-format, whereas speech is currently only CJS. This guarantees gax works with both cjs and libraries published in dual format.
  • Unit tests needed to be refactored to not use sinon. Instead we use esmock for the most part, which gets compiled into proxyquire in cjs.
  • Stubs were heavily used with the warnings module. Instead we are listening for actual warnings, and asserting the warnings when they are emitted.
  • Added node 22 tests

Tools

  • compileProtos in tools now needs to get protos from one directory level higher given the new ESM level in the directoru structure.
  • Since ES modules export modules and not objects, we need to access protobufjs exports from the default property, so we need to add the line \nconst $protobuf = protobuf.default to all of our protos; this will happen when we compileProtos, which is why we'll need to rerun before submitting.
  • The protobuf export gax exports needs to now be exported from the correct build directory.

Migration Guide

  • For our client libraries: we will need to add an @ts-ignore tag to the generated code importing gax, specifically: this._gaxModule = opts.fallback ? gaxInstance.fallback : gaxInstance; in the client.ts; the reason is that client libraries, in their CJS incarnation, optionally asynchronously load gax in the line before. However since it is not imported at the top of the file, ESM thinks it's not being loaded, when in fact in ESM it is not conditionally required (it is just asynchronously loaded due to being a regular ESM import). TLDR: TS doesn't realize gax will always be loaded, so we need to ignore the warning. We must add this to our generated client library code.
  • Also, we will need to add ["allowSyntheticDefaultImports": true](https://www.typescriptlang.org/tsconfig/#allowSyntheticDefaultImports) in our tsconfigs to appropriately load gax.
  • Lastly, we'll need to re-add compileProtos before running tests in system tests. Since we are slightly modifying how we compileProtos, we'll need to recompile before running tests.

Package.json changes

  • main and types default to CJS, while the module is actually overall type as module (i.e., esm). @danielbankhead, do you have an example of where this won't work (that we claim to support)? This would be an awesome test to add in if so! Reason I'm asking is because this design is what we're doing for our other repos, specifically all our client libraries (see storage), so it's worth confirming if this actually won't work.
  • Exports and imports are specified for all likely export paths, which will vary based on whether the user is using CJS or ESM.
  • We are adding babel dependencies to compile ESM code to CJS.
  • All test commands are doubled - one for CJS and one for ESM.
  • Compile commands are also doubled. For ESM, we 1) run TS, 2) copy over any *.json files, 3) copy over tests, 4) do some slight find-and-replace functions to make protos esm-friendly, 5) copy over the protos to build. For CJS, we 1) run TS (the CJS version), 2) Run babel, 3) add our custom post-processing command (that adds a .cjs extension for files mocked in proxyquire), 4) copy over any *.json files, 5) copy over test fixtures.
  • Proto compilation commands are doubled, one for CJS and one for ESM. The key difference is that ESM adds this -w es6 command, which outputs the JS to ES6 instead of UMD.
  • Most other commands need to add one more level of directory structure due to the new esm directory level
  • The babel command which is described in Publishing Nodejs libraries in ESM and CJS: basically takes the TS output and runs some custom babel plugins, as well as copies over files, and puts files into the cjs directory.
  • I removed the browser field, and removed the module.exports code in fallback.js (the previous browser entry point). this was there to ensure fallback would work in esm and cjs contexts; but, since we now have separate entry points for both, we can just remove all this code entirely.

TODO for after:

  • system tests rely on published versions of speech and tasks. Given those changes described in the migration guide, that haven't been published in those libraries, we are commenting out the part that grabs the latest versions of the libraries and instead using its current code in the monorepo. This doesn't change the integrity of the tests at all, it's just slightly less safe in that published versions of libraries are guaranteed to have passing tests vs. current code. We just need to uncomment lines 49 - 86 and 118 -122 in gax/esm/test/system-test/test.clientlibs.ts
  • CompileProtos needs to be republished in order to unskip the skipped test that tests where gaxProtos live. This test is failing because of a circular dependency: the current published version of gax doesn't contain this new directory level, so we can't test it appropriately. After publishing dual-format, we can re-enable this test to see it works.In the meantime, since client libraries use compileProtos, we'll see it work correctly in system tests with the current version of gax.

@sofisl sofisl requested review from a team as code owners December 5, 2024 23:41
@product-auto-label product-auto-label bot added the size: l Pull request size is large. label Dec 5, 2024
gax/esm/test/unit/regapic.ts Dismissed Show dismissed Hide dismissed
Copy link

generated-files-bot bot commented Dec 6, 2024

Warning: This pull request is touching the following templated files:

  • .kokoro/common.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node14/common.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node14/lint.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node14/samples-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node14/system-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node14/test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/continuous/node18/browser-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/presubmit/node14/common.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/presubmit/node14/samples-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/presubmit/node14/system-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/presubmit/node14/test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/presubmit/node18/browser-test.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/release/docs.cfg - .kokoro files are templated and should be updated in synthtool
  • .kokoro/samples-test.sh - .kokoro files are templated and should be updated in synthtool
  • .kokoro/system-test.sh - .kokoro files are templated and should be updated in synthtool
  • .kokoro/test.bat - .kokoro files are templated and should be updated in synthtool
  • .kokoro/test.sh - .kokoro files are templated and should be updated in synthtool
  • .github/workflows/ci.yaml - .github/workflows/ci.yaml (GitHub Actions) should be updated in synthtool

@sofisl sofisl added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Dec 6, 2024
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Dec 6, 2024
Comment on lines +5 to +7
"main": "./build/cjs/src/index.cjs",
"type": "module",
"types": "./build/cjs/src/index.d.ts",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick note: the main should be .js/.mjs when type: esm - some, namely older, build tools won't work well with the conflict. Alternatively, it may be easier to use type: commonjs as the default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielbankhead, do you have an example of where this won't work (that we claim to support)? This would be an awesome test to add in if so! Reason I'm asking is because this design is what we're doing for our other repos, specifically all our client libraries (see storage), so it's worth confirming if this actually won't work.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Webpack 4- and earlier versions of eslint will fail here.

Additionally, in the storage example the main ends with .js rather than .cjs.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think of it not whether we claim to support it or not, but rather are we following guidance and standards appropriately. If not, we can expect some customers to run into friction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, isn't storage still using a cjs file (seems like the js file is in the cjs module?) Also we are running tests using webpack 4 for the browser and they are WAI!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless, I don't feel strongly about this since we're testing gax in our system tests, and I think the exports field have precedence over main anyways. Running tests to make sure everything passes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,50 @@
const fs = require('fs');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@sofisl sofisl Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for changing the extension in proxyquire calls, so it wouldn't be affected by a package.json, although I see your larger point that we wouldn't even have to do that if we didn't rename the files at all. It seems slightly cleaner/safer to me to have the .cjs and .js distinguishing the files, easier to see at a glance what's going on, but that's just my perspective!

@sofisl sofisl changed the title Migrate to esm chore!: publish gax-nodejs in dual format (CJS and ESM) Dec 12, 2024
@sofisl sofisl requested a review from d-goog December 12, 2024 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size: l Pull request size is large.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants