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

First class ESM support #113

Merged
merged 19 commits into from
Oct 17, 2023
Merged

First class ESM support #113

merged 19 commits into from
Oct 17, 2023

Conversation

raphael-theriault-swi
Copy link
Member

@raphael-theriault-swi raphael-theriault-swi commented Oct 12, 2023

Use Rollup to target both ESM and CJS from most packages, using conditional exports to specify which entrypoint to use depending on the format.

Exports OTel's somewhat hidden hooks.mjs which themselves export import-in-the-middle hooks and provides both support for Node <20 with --loader flag and >=20 with --import flag. The old -r flag will still work for CJS but doesn't support ESM at all, which the others support both ESM and CJS.

Most of the examples have been migrated to ESM, I'm planning to do the same with scripts eventually.

Copy link
Member Author

Choose a reason for hiding this comment

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

Required for Rollup not to hang at the end of the build for no apparent reason, probably due to an underlying bug in the TypeScript compiler but I don't want to open that can of worms. Created a PR on the Rollup repo to add this as a CLI flag rollup/rollup#5195

Comment on lines +115 to +116
await app.listen({ port, host: "0.0.0.0" })
await app.pg.query(schema)
Copy link
Member Author

Choose a reason for hiding this comment

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

ESM support gets us top level await

Copy link
Member Author

Choose a reason for hiding this comment

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

ESLint configs in directories with a package.json with "type": "module" all have to be converted to ESM syntax

Comment on lines +33 to +43
try {
module.exports.oboe = require(`@solarwinds-apm/bindings-${t}/oboe.node`)
} catch (cause) {
module.exports.oboe = new Error(`unsupported platform ${t}`, { cause })
}

const e = (module.exports = {})
nativeRequireAssign("oboe", e)
nativeRequireAssign("metrics", e)
try {
module.exports.metrics = require(`@solarwinds-apm/bindings-${t}/metrics.node`)
} catch (cause) {
module.exports.metrics = new Error(`unsupported platform ${t}`, { cause })
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Laying things out this way helps Node detect the exports during static analysis for ESM/CJS interop

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

export namespace eventLoop {
export declare namespace eventLoop {
Copy link
Member Author

Choose a reason for hiding this comment

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

Wrote those by hands and made a mistake, apparently this is the proper syntax.

Comment on lines 24 to 27
"files": [
"dist/"
"./src/",
"./dist/"
],
Copy link
Member Author

Choose a reason for hiding this comment

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

Including src in the published artifacts and generating sourcemaps in dist makes debugging much nicer. Previously we generated the sourcemaps but didn't bundle the source so they pointed to files that didn't exist.

Comment on lines +17 to +18
import { collectNodeModulesDependencies } from "./node-modules.js"
import { collectPnpApiDependencies, type PnpApi } from "./pnp-api.js"
Copy link
Member Author

Choose a reason for hiding this comment

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

ESM requires full file name and extensions in import. We use .js and not .ts cause TypeScript (by design) never touches file extensions in imports.

Comment on lines -128 to -130
// ts declarations
{
files: ["**/*.d.ts"],
Copy link
Member Author

Choose a reason for hiding this comment

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

This was only required because I was missing the declare keywords in manually written .d.ts files.

Comment on lines 58 to -66
import {
awsBeanstalkDetector,
awsEc2Detector,
awsEcsDetector,
awsEksDetector,
awsLambdaDetector,
} from "@opentelemetry/resource-detector-aws"
import { containerDetector } from "@opentelemetry/resource-detector-container"
import { gcpDetector } from "@opentelemetry/resource-detector-gcp"
Copy link
Member Author

Choose a reason for hiding this comment

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

Some of these detectors were spamming warnings, better just add them back if someone ever requests it.

Comment on lines -17 to -21
const TOKEN = "secret"
import base from "@solarwinds-apm/eslint-config"

module.exports = {
TOKEN,
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This wasn't used in the example anymore and I guess git decided I actually moved the file and changed everything in it.

Comment on lines -53 to -59
"@opentelemetry/instrumentation-bunyan": "^0.32.1",
"@opentelemetry/instrumentation-fs": "^0.8.1",
"@opentelemetry/instrumentation-http": "~0.43.0",
"@opentelemetry/instrumentation-mysql2": "^0.34.1",
"@opentelemetry/instrumentation-pg": "^0.36.1",
"@opentelemetry/instrumentation-pino": "^0.34.1",
"@opentelemetry/instrumentation-winston": "^0.32.1",
Copy link
Member Author

Choose a reason for hiding this comment

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

This was to be able to import types but now we already have access to those types from the instrumentations package's dependencies.

Copy link
Member Author

Choose a reason for hiding this comment

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

I created this cause OTel's DiagConsoleLogger doesn't show levels or timestamps and it was making my life very painful.

Copy link
Member Author

Choose a reason for hiding this comment

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

Without this the lambda instrumentation always prints a warning when used outside lambda.

Comment on lines 56 to 58
"engines": {
"node": ">=16"
"node": ">=16.13.0"
},
Copy link
Member Author

Choose a reason for hiding this comment

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

Node 16.13 is the first Node 16 LTS and it properly supports ESM. Every previous 16.x release has been unsupported for years.

Comment on lines 32 to 35
if (setRegister && semver.gte(process.versions.node, "20.8.0")) {
setRegister()
module.register("./hooks.es.js", import.meta.url)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

The --loader flag is deprecated in Node >=20.8 in favour of this new function.

Comment on lines 52 to +56
export async function init() {
const id = `${packageJson.name}@${packageJson.version}`
const initSymbol = Symbol.for(`${id}/init`)

if (!(initSymbol in globalThis)) {
Object.defineProperty(globalThis, initSymbol, {
value: true,
writable: false,
enumerable: false,
configurable: false,
})
// init only once
const setInit = setter("init")
if (!setInit) return
setInit()
Copy link
Member Author

Choose a reason for hiding this comment

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

Big diff here but it's just an early return and everything being moved out of the if statement and losing an indentation level.

@raphael-theriault-swi raphael-theriault-swi requested a review from a team October 13, 2023 21:57
@raphael-theriault-swi raphael-theriault-swi marked this pull request as ready for review October 13, 2023 21:57

Packages should optimally target both ESM and CJS unless they have a good reason not to, in which case they should target CJS only for compatibility. At the moment the two CJS-only packages are the SDK because it's internal and has a lot of stateful components, and the bindings because they are internal and `.node` files can't be imported from ESM.

Dual targeting is made simple by the project's [Rollup configuration](./packages/rollup-config/), which should work without being extended pretty much all the time. Files ending in `.es.{ts, js}` will only be included for ESM `.cjs.{ts, js}` only for CJS. We don't use `.m{ts, js}` and `.c{ts, js}` because they have poor TypeScript support (and I, the original author, think they are Ugly and Bad). Projects should always specify `"type": "module"` when dual targeting (or an explicit `"commonjs"`) in their `package.json`.

Choose a reason for hiding this comment

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

Ugly and Bad lmao

Copy link

@tammy-baylis-swi tammy-baylis-swi left a comment

Choose a reason for hiding this comment

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

Lgtm! I also like how much flatter this is. Thank you for all the comments ahead of time, and awesome for also putting in the Rollup PR.

@raphael-theriault-swi raphael-theriault-swi merged commit cc83799 into main Oct 17, 2023
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants