diff --git a/examples/README.md b/examples/README.md index c594e5f442..dcef5e8d8e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,6 +17,7 @@ use the latest and greatest features, and best practices. | [grpc](grpc/) | gRPC Instrumentation to automatically collect trace data and export them to the backend of choice | Intermediate | | [otlp-exporter-node](otlp-exporter-node/) | This example shows how to use `@opentelemetry/exporter-otlp-http` to instrument a simple Node.js application | Intermediate | | [opentracing-shim](opentracing-shim/) | This is a simple example that demonstrates how existing OpenTracing instrumentation can be integrated with OpenTelemetry | Intermediate | +| [esm-http-ts](esm-http-ts/) | This is a simple example that demonstrates tracing HTTP request, with an app written in TypeScript and transpiled to ES Modules. | Intermediate | Examples of experimental packages can be found at [experimental/examples](../experimental/examples). diff --git a/examples/esm-http-ts/README.md b/examples/esm-http-ts/README.md new file mode 100644 index 0000000000..72f0e2dbf4 --- /dev/null +++ b/examples/esm-http-ts/README.md @@ -0,0 +1,25 @@ +# Overview + +This is a simple example that demonstrates tracing HTTP request, with an app written in TypeScript and transpiled to ES Modules. + +## Installation + +```sh +# from this directory +npm install +npm run build +npm start +``` + +In a separate terminal, `curl localhost:3000`. + +See two spans in the console (one manual, one for http instrumentation) + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry for Node.js, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/esm-http-ts/index.ts b/examples/esm-http-ts/index.ts new file mode 100644 index 0000000000..b4719a8bc7 --- /dev/null +++ b/examples/esm-http-ts/index.ts @@ -0,0 +1,43 @@ +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { trace, DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import http from 'http'; + +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); +const tracerProvider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'esm-http-ts-example', + }), +}); +const exporter = new ConsoleSpanExporter(); +const processor = new SimpleSpanProcessor(exporter); +tracerProvider.addSpanProcessor(processor); +tracerProvider.register(); + +registerInstrumentations({ + instrumentations: [new HttpInstrumentation()], +}); + +const hostname = '0.0.0.0'; +const port = 3000; + +const server = http.createServer((req, res) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + const tracer = trace.getTracer('esm-tracer'); + tracer.startActiveSpan('manual', span => { + span.end(); + }); + res.end('Hello, World!\n'); +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); diff --git a/examples/esm-http-ts/package.json b/examples/esm-http-ts/package.json new file mode 100644 index 0000000000..49c0e2601f --- /dev/null +++ b/examples/esm-http-ts/package.json @@ -0,0 +1,42 @@ +{ + "name": "esm-http-ts", + "private": true, + "version": "0.38.0", + "description": "Example of HTTP integration with OpenTelemetry using ESM and TypeScript", + "main": "build/index.js", + "type": "module", + "scripts": { + "build": "tsc --build", + "start": "node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./build/index.js" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "http", + "tracing", + "esm", + "typescript" + ], + "engines": { + "node": ">=14" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/", + "dependencies": { + "@opentelemetry/api": "1.4.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.38.0", + "@opentelemetry/instrumentation": "0.38.0", + "@opentelemetry/instrumentation-http": "0.38.0", + "@opentelemetry/resources": "1.9.1", + "@opentelemetry/sdk-trace-base": "1.9.1", + "@opentelemetry/sdk-trace-node": "1.9.1", + "@opentelemetry/semantic-conventions": "1.9.1" + } +} diff --git a/examples/esm-http-ts/tsconfig.json b/examples/esm-http-ts/tsconfig.json new file mode 100644 index 0000000000..5f821d66c7 --- /dev/null +++ b/examples/esm-http-ts/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + "rootDir": "." /* Specify the root folder within your source files. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "resolveJsonModule": true /* Enable importing .json files. */, + + /* Emit */ + "outDir": "build" /* Specify an output folder for all emitted files. */, + + /* Interop Constraints */ + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Completeness */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["**/*.ts", "**/*.js", "*.config.js"], + "exclude": ["node_modules"] +} diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index bed394a8f3..398fa659e8 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(instrumentation): add ESM support for instrumentation. [#3698](https://github.com/open-telemetry/opentelemetry-js/pull/3698) @JamieDanielson, @pkanal, @vmarchaud, @lizthegrey, @bengl + ### :bug: (Bug Fix) ### :books: (Refine Doc) diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index 0b5c166dd5..15ca92a45e 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -66,7 +66,6 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; export class HttpInstrumentation extends InstrumentationBase { /** keep track on spans not ended */ private readonly _spanNotEnded: WeakSet = new WeakSet(); - private readonly _version = process.versions.node; private _headerCapture; private _httpServerDurationHistogram!: Histogram; private _httpClientDurationHistogram!: Histogram; @@ -112,11 +111,12 @@ export class HttpInstrumentation extends InstrumentationBase { } private _getHttpInstrumentation() { + const version = process.versions.node; return new InstrumentationNodeModuleDefinition( 'http', ['*'], moduleExports => { - this._diag.debug(`Applying patch for http@${this._version}`); + this._diag.debug(`Applying patch for http@${version}`); if (isWrapped(moduleExports.request)) { this._unwrap(moduleExports, 'request'); } @@ -145,7 +145,7 @@ export class HttpInstrumentation extends InstrumentationBase { }, moduleExports => { if (moduleExports === undefined) return; - this._diag.debug(`Removing patch for http@${this._version}`); + this._diag.debug(`Removing patch for http@${version}`); this._unwrap(moduleExports, 'request'); this._unwrap(moduleExports, 'get'); @@ -155,11 +155,12 @@ export class HttpInstrumentation extends InstrumentationBase { } private _getHttpsInstrumentation() { + const version = process.versions.node; return new InstrumentationNodeModuleDefinition( 'https', ['*'], moduleExports => { - this._diag.debug(`Applying patch for https@${this._version}`); + this._diag.debug(`Applying patch for https@${version}`); if (isWrapped(moduleExports.request)) { this._unwrap(moduleExports, 'request'); } @@ -188,7 +189,7 @@ export class HttpInstrumentation extends InstrumentationBase { }, moduleExports => { if (moduleExports === undefined) return; - this._diag.debug(`Removing patch for https@${this._version}`); + this._diag.debug(`Removing patch for https@${version}`); this._unwrap(moduleExports, 'request'); this._unwrap(moduleExports, 'get'); diff --git a/experimental/packages/opentelemetry-instrumentation/README.md b/experimental/packages/opentelemetry-instrumentation/README.md index 682fc5741a..6c27e3d36d 100644 --- a/experimental/packages/opentelemetry-instrumentation/README.md +++ b/experimental/packages/opentelemetry-instrumentation/README.md @@ -219,12 +219,18 @@ If nothing is specified the global registered provider is used. Usually this is There might be usecase where someone has the need for more providers within an application. Please note that special care must be takes in such setups to avoid leaking information from one provider to the other because there are a lot places where e.g. the global `ContextManager` or `Propagator` is used. +## Instrumentation for ES Modules In NodeJS (experimental) + +As the module loading mechanism for ESM is different than CJS, you need to select a custom loader so instrumentation can load hook on the esm module it want to patch. To do so, you must provide the `--experimental-loader=@opentelemetry/instrumentation/hook.mjs` flag to the `node` binary. Alternatively you can set the `NODE_OPTIONS` environment variable to `NODE_OPTIONS="--experimental-loader=@opentelemetry/instrumentation/hook.mjs"`. +As the ESM module loader from NodeJS is experimental, so is our support for it. Feel free to provide feedback or report issues about it. + +**Note**: ESM Instrumentation is not yet supported for Node 20. + ## Limitations -Instrumentations for external modules (e.g. express, mongodb,...) hooks the `require` call. Therefore following conditions need to be met that this mechanism can work: +Instrumentations for external modules (e.g. express, mongodb,...) hooks the `require` call or `import` statement. Therefore following conditions need to be met that this mechanism can work: -* `require` is used. ECMA script modules (using `import`) is not supported as of now -* Instrumentations are registered **before** the module to instrument is `require`ed +* Instrumentations are registered **before** the module to instrument is `require`ed (CJS only) * modules are not included in a bundle. Tools like `esbuild`, `webpack`, ... usually have some mechanism to exclude specific modules from bundling ## License diff --git a/experimental/packages/opentelemetry-instrumentation/hook.mjs b/experimental/packages/opentelemetry-instrumentation/hook.mjs new file mode 100644 index 0000000000..7111b37699 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/hook.mjs @@ -0,0 +1,28 @@ +/*! + * Copyright 2021 Datadog, Inc. + * Copyright The OpenTelemetry Authors + * + * 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 + * + * http://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. + */ + +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. + +import { + load, + resolve, + getFormat, + getSource, +} from 'import-in-the-middle/hook.mjs'; +export { load, resolve, getFormat, getSource }; diff --git a/experimental/packages/opentelemetry-instrumentation/package.json b/experimental/packages/opentelemetry-instrumentation/package.json index 13f78ba0b3..4164952821 100644 --- a/experimental/packages/opentelemetry-instrumentation/package.json +++ b/experimental/packages/opentelemetry-instrumentation/package.json @@ -32,6 +32,7 @@ "build/src/**/*.js", "build/src/**/*.js.map", "build/src/**/*.d.ts", + "hook.mjs", "doc", "LICENSE", "README.md" @@ -47,7 +48,9 @@ "tdd": "npm run tdd:node", "tdd:node": "npm run test -- --watch-extensions ts --watch", "tdd:browser": "karma start", - "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'", + "test:cjs": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'", + "test:esm": "nyc node --experimental-loader=./hook.mjs ../../../node_modules/mocha/bin/mocha 'test/node/*.test.mjs' test/node/*.test.mjs", + "test": "npm run test:cjs && npm run test:esm", "test:browser": "nyc karma start --single-run", "version": "node ../../../scripts/version-update.js", "watch": "tsc --build --watch tsconfig.json tsconfig.esm.json tsconfig.esnext.json", @@ -68,6 +71,8 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.3.5", "require-in-the-middle": "^7.1.0", "semver": "^7.3.2", "shimmer": "^1.2.1" @@ -82,7 +87,6 @@ "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/semver": "7.3.9", - "@types/shimmer": "1.0.2", "@types/sinon": "10.0.13", "@types/webpack-env": "1.16.3", "babel-loader": "8.2.3", diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index 5790c9564e..03d8f6ba37 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -16,12 +16,16 @@ import * as types from '../../types'; import * as path from 'path'; +import { types as utilTypes } from 'util'; import { satisfies } from 'semver'; +import { wrap, unwrap, massWrap, massUnwrap } from 'shimmer'; import { InstrumentationAbstract } from '../../instrumentation'; import { RequireInTheMiddleSingleton, Hooked, } from './RequireInTheMiddleSingleton'; +import type { HookFn } from 'import-in-the-middle'; +import * as ImportInTheMiddle from 'import-in-the-middle'; import { InstrumentationModuleDefinition } from './types'; import { diag } from '@opentelemetry/api'; import type { OnRequireFn } from 'require-in-the-middle'; @@ -68,6 +72,75 @@ export abstract class InstrumentationBase } } + protected override _wrap: typeof wrap = (moduleExports, name, wrapper) => { + if (!utilTypes.isProxy(moduleExports)) { + return wrap(moduleExports, name, wrapper); + } else { + const wrapped = wrap(Object.assign({}, moduleExports), name, wrapper); + + return Object.defineProperty(moduleExports, name, { + value: wrapped, + }); + } + }; + + protected override _unwrap: typeof unwrap = (moduleExports, name) => { + if (!utilTypes.isProxy(moduleExports)) { + return unwrap(moduleExports, name); + } else { + return Object.defineProperty(moduleExports, name, { + value: moduleExports[name], + }); + } + }; + + protected override _massWrap: typeof massWrap = ( + moduleExportsArray, + names, + wrapper + ) => { + if (!moduleExportsArray) { + diag.error('must provide one or more modules to patch'); + return; + } else if (!Array.isArray(moduleExportsArray)) { + moduleExportsArray = [moduleExportsArray]; + } + + if (!(names && Array.isArray(names))) { + diag.error('must provide one or more functions to wrap on modules'); + return; + } + + moduleExportsArray.forEach(moduleExports => { + names.forEach(name => { + this._wrap(moduleExports, name, wrapper); + }); + }); + }; + + protected override _massUnwrap: typeof massUnwrap = ( + moduleExportsArray, + names + ) => { + if (!moduleExportsArray) { + diag.error('must provide one or more modules to patch'); + return; + } else if (!Array.isArray(moduleExportsArray)) { + moduleExportsArray = [moduleExportsArray]; + } + + if (!(names && Array.isArray(names))) { + diag.error('must provide one or more functions to wrap on modules'); + return; + } + + moduleExportsArray.forEach(moduleExports => { + names.forEach(name => { + this._unwrap(moduleExports, name); + }); + }); + }; + private _warnOnPreloadedModules(): void { this._modules.forEach((module: InstrumentationModuleDefinition) => { const { name } = module; @@ -101,7 +174,7 @@ export abstract class InstrumentationBase module: InstrumentationModuleDefinition, exports: T, name: string, - baseDir?: string + baseDir?: string | void ): T { if (!baseDir) { if (typeof module.patch === 'function') { @@ -168,6 +241,14 @@ export abstract class InstrumentationBase this._warnOnPreloadedModules(); for (const module of this._modules) { + const hookFn: HookFn = (exports, name, baseDir) => { + return this._onRequire( + module as unknown as InstrumentationModuleDefinition, + exports, + name, + baseDir + ); + }; const onRequire: OnRequireFn = (exports, name, baseDir) => { return this._onRequire( module as unknown as InstrumentationModuleDefinition, @@ -185,6 +266,13 @@ export abstract class InstrumentationBase : this._requireInTheMiddleSingleton.register(module.name, onRequire); this._hooks.push(hook); + const esmHook = + new (ImportInTheMiddle as unknown as typeof ImportInTheMiddle.default)( + [module.name], + { internals: false }, + hookFn + ); + this._hooks.push(esmHook); } } diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/EsmInstrumentation.test.mjs b/experimental/packages/opentelemetry-instrumentation/test/node/EsmInstrumentation.test.mjs new file mode 100644 index 0000000000..f09097cd79 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/node/EsmInstrumentation.test.mjs @@ -0,0 +1,138 @@ +/* + * Copyright The OpenTelemetry Authors + * + * 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. + */ + +import * as assert from 'assert'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, +} from '../../build/src/index.js'; +import * as exported from 'test-esm-module'; + +class TestInstrumentationWrapFn extends InstrumentationBase { + constructor(config) { + super('test-esm-instrumentation', '0.0.1', config); + } + init() { + console.log('test-esm-instrumentation initialized!'); + return new InstrumentationNodeModuleDefinition( + 'test-esm-module', + ['*'], + moduleExports => { + this._wrap(moduleExports, 'testFunction', () => { + return () => 'patched'; + }); + return moduleExports; + }, + moduleExports => { + this._unwrap(moduleExports, 'testFunction'); + return moduleExports; + } + ); + } +} + +class TestInstrumentationMasswrapFn extends InstrumentationBase { + constructor(config) { + super('test-esm-instrumentation', '0.0.1', config); + } + init() { + console.log('test-esm-instrumentation initialized!'); + return new InstrumentationNodeModuleDefinition( + 'test-esm-module', + ['*'], + moduleExports => { + this._massWrap( + [moduleExports], + ['testFunction', 'secondTestFunction'], + () => { + return () => 'patched'; + } + ); + return moduleExports; + }, + moduleExports => { + this._massUnwrap( + [moduleExports], + ['testFunction', 'secondTestFunction'] + ); + return moduleExports; + } + ); + } +} + +class TestInstrumentationSimple extends InstrumentationBase { + constructor(config) { + super('test-esm-instrumentation', '0.0.1', config); + } + init() { + console.log('test-esm-instrumentation initialized!'); + return new InstrumentationNodeModuleDefinition( + 'test-esm-module', + ['*'], + moduleExports => { + moduleExports.testConstant = 43; + return moduleExports; + } + ); + } +} +describe('when loading esm module', () => { + const instrumentationWrap = new TestInstrumentationWrapFn({ + enabled: false, + }); + + it('should patch module file directly', async () => { + const instrumentation = new TestInstrumentationSimple({ + enabled: false, + }); + instrumentation.enable(); + assert.deepEqual(exported.testConstant, 43); + }); + + it('should patch a module with the wrap function', async () => { + instrumentationWrap.enable(); + assert.deepEqual(exported.testFunction(), 'patched'); + }); + + it('should unwrap a patched function', async () => { + instrumentationWrap.enable(); + // disable to trigger unwrap + instrumentationWrap.disable(); + assert.deepEqual(exported.testFunction(), 'original'); + }); + + it('should wrap multiple functions with masswrap', () => { + const instrumentation = new TestInstrumentationMasswrapFn({ + enabled: false, + }); + + instrumentation.enable(); + assert.deepEqual(exported.testFunction(), 'patched'); + assert.deepEqual(exported.secondTestFunction(), 'patched'); + }); + + it('should unwrap multiple functions with massunwrap', () => { + const instrumentation = new TestInstrumentationMasswrapFn({ + enabled: false, + }); + + instrumentation.enable(); + instrumentation.disable(); + assert.deepEqual(exported.testFunction(), 'original'); + assert.deepEqual(exported.secondTestFunction(), 'original'); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/.gitkeep b/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/package.json b/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/package.json new file mode 100644 index 0000000000..3caeae666d --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-esm-module", + "version": "0.1.0", + "main": "./src/index.js", + "type": "module" +} diff --git a/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/src/index.js b/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/src/index.js new file mode 100644 index 0000000000..2fb6c6ed25 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/node/node_modules/test-esm-module/src/index.js @@ -0,0 +1,9 @@ +export const testFunction = () => { + return 'original'; +}; + +export const secondTestFunction = () => { + return 'original'; +}; + +export const testConstant = 42; diff --git a/lerna.json b/lerna.json index 6eba3f4874..14fb76982e 100644 --- a/lerna.json +++ b/lerna.json @@ -12,6 +12,7 @@ "examples/otlp-exporter-node", "examples/opentelemetry-web", "examples/http", - "examples/https" + "examples/https", + "examples/esm-http-ts" ] } diff --git a/scripts/update-ts-configs.js b/scripts/update-ts-configs.js index 283811997b..3c4f43273f 100644 --- a/scripts/update-ts-configs.js +++ b/scripts/update-ts-configs.js @@ -52,6 +52,7 @@ const ignoredLernaProjects = [ 'examples/opentelemetry-web', 'examples/http', 'examples/https', + 'examples/esm-http-ts', ]; let dryRun = false;