diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index d553ba1c..2d6ebcfd 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -108,6 +108,7 @@ pipeline { withNodeJSEnv(){ dir("${BASE_DIR}"){ sh(label: 'install jest-unit',script: 'npm add --dev jest-junit') + sh(label: 'install e2e bundling deps',script: 'npm install --prefix ./__tests__/e2e') sh(label: 'Runs the tests',script: './node_modules/.bin/jest --ci --reporters=default --reporters=jest-junit') } } diff --git a/__tests__/e2e/package.json b/__tests__/e2e/package.json index 7a49abcb..cc33e959 100644 --- a/__tests__/e2e/package.json +++ b/__tests__/e2e/package.json @@ -15,6 +15,5 @@ "@elastic/synthetics": "file:../../", "axios": "^0.21.0", "semver": "^7.3.5" - }, - "dependencies": {} + } } diff --git a/__tests__/push/bundler.test.ts b/__tests__/push/bundler.test.ts index 81cbe7cb..a5279d82 100644 --- a/__tests__/push/bundler.test.ts +++ b/__tests__/push/bundler.test.ts @@ -30,26 +30,44 @@ import { join } from 'path'; import { generateTempPath } from '../../src/helpers'; import { Bundler } from '../../src/push/bundler'; +const PROJECT_DIR = join(__dirname, 'test-bundler'); +const journeyFile = join(PROJECT_DIR, 'bundle.journey.ts'); + async function validateZip(content) { + const partialPath = join( + '__tests__', + 'push', + 'test-bundler', + 'bundle.journey.ts' + ); const decoded = Buffer.from(content, 'base64'); const pathToZip = generateTempPath(); await writeFile(pathToZip, decoded); const files = []; + const entries = createReadStream(pathToZip).pipe( unzipper.Parse({ forceStream: true }) ); + + let targetFileContent = null; for await (const entry of entries) { files.push(entry.path); + + if (entry.path === partialPath) { + entry.on('data', d => (targetFileContent += d)); + } } - expect(files).toEqual(['__tests__/push/test-bundler/bundle.journey.ts']); + expect(files).toEqual([partialPath]); + + expect(targetFileContent).toContain('__toESM'); + expect(targetFileContent).toContain('node_modules/is-positive/index.js'); + await unlink(pathToZip); } describe('Bundler', () => { - const PROJECT_DIR = join(__dirname, 'test-bundler'); - const journeyFile = join(PROJECT_DIR, 'bundle.journey.ts'); const bundler = new Bundler(); beforeAll(async () => { @@ -57,9 +75,13 @@ describe('Bundler', () => { await writeFile( journeyFile, `import {journey, step, monitor} from '@elastic/synthetics'; +import isPositive from 'is-positive'; + journey('journey 1', () => { monitor.use({ id: 'duplicate id' }) - step("step1", () => {}) + step("step1", () => { + isPositive(-1); + }) });` ); }); diff --git a/__tests__/push/plugin.test.ts b/__tests__/push/plugin.test.ts index 5ae5d301..f11ee670 100644 --- a/__tests__/push/plugin.test.ts +++ b/__tests__/push/plugin.test.ts @@ -25,6 +25,7 @@ import * as esbuild from 'esbuild'; import { join } from 'path'; +import NodeResolve from '@esbuild-plugins/node-resolve'; import { MultiAssetPlugin, commonOptions, @@ -43,7 +44,13 @@ describe('Plugin', () => { await esbuild.build({ ...commonOptions(), entryPoints: [join(E2E_DIR, 'uptime.journey.ts')], - plugins: [MultiAssetPlugin(callback)], + plugins: [ + MultiAssetPlugin(callback), + NodeResolve({ + extensions: ['.ts', '.js'], + resolveOptions: { basedir: E2E_DIR }, + }), + ], }); }); }); diff --git a/package-lock.json b/package-lock.json index c5236e9a..2c9699cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.1", + "@esbuild-plugins/node-resolve": "^0.1.4", "archiver": "^5.3.0", "commander": "^9.0.0", "deepmerge": "^4.2.2", @@ -41,6 +42,7 @@ "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", "husky": "^4.3.6", + "is-positive": "3.1.0", "jest": "^28.1.3", "lint-staged": "^10.5.3", "prettier": "^2.4.1", @@ -645,6 +647,31 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild-plugins/node-resolve": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz", + "integrity": "sha512-haFQ0qhxEpqtWWY0kx1Y5oE3sMyO1PcoSiWEPrAw6tm/ZOOLXjSs6Q+v1v9eyuVF0nNt50YEvrcrvENmyoMv5g==", + "dependencies": { + "@types/resolve": "^1.17.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0", + "resolve": "^1.19.0" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-resolve/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.3.tgz", @@ -1330,6 +1357,11 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + }, "node_modules/@types/sharp": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.28.2.tgz", @@ -3530,9 +3562,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "funding": [ { "type": "individual", @@ -3602,8 +3634,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -3817,7 +3848,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4087,7 +4117,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -4150,6 +4179,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-positive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-positive/-/is-positive-3.1.0.tgz", + "integrity": "sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -5566,8 +5604,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6008,7 +6045,6 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -6540,7 +6576,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7558,6 +7593,24 @@ } } }, + "@esbuild-plugins/node-resolve": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.1.4.tgz", + "integrity": "sha512-haFQ0qhxEpqtWWY0kx1Y5oE3sMyO1PcoSiWEPrAw6tm/ZOOLXjSs6Q+v1v9eyuVF0nNt50YEvrcrvENmyoMv5g==", + "requires": { + "@types/resolve": "^1.17.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0", + "resolve": "^1.19.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, "@eslint/eslintrc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.3.tgz", @@ -8131,6 +8184,11 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + }, "@types/sharp": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.28.2.tgz", @@ -9642,9 +9700,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "fs-constants": { "version": "1.0.0", @@ -9689,8 +9747,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -9853,7 +9910,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -10042,7 +10098,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -10084,6 +10139,12 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-positive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-positive/-/is-positive-3.1.0.tgz", + "integrity": "sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==", + "dev": true + }, "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -11164,8 +11225,7 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-type": { "version": "4.0.0", @@ -11480,7 +11540,6 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, "requires": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -11849,8 +11908,7 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "tar-fs": { "version": "2.1.1", diff --git a/package.json b/package.json index 3478111a..cbd8cb36 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.1", + "@esbuild-plugins/node-resolve": "^0.1.4", "archiver": "^5.3.0", "commander": "^9.0.0", "deepmerge": "^4.2.2", @@ -68,6 +69,7 @@ "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", "husky": "^4.3.6", + "is-positive": "3.1.0", "jest": "^28.1.3", "lint-staged": "^10.5.3", "prettier": "^2.4.1", diff --git a/src/push/bundler.ts b/src/push/bundler.ts index d54edd63..bb32dd7e 100644 --- a/src/push/bundler.ts +++ b/src/push/bundler.ts @@ -24,12 +24,15 @@ */ import path from 'path'; -import { unlink, readFile } from 'fs/promises'; +import { stat, unlink, readFile } from 'fs/promises'; import { createWriteStream } from 'fs'; import * as esbuild from 'esbuild'; +import NodeResolve from '@esbuild-plugins/node-resolve'; import archiver from 'archiver'; import { commonOptions, MultiAssetPlugin, PluginData } from './plugin'; +const SIZE_LIMIT_KB = 800; + function relativeToCwd(entry: string) { return path.relative(process.cwd(), entry); } @@ -49,7 +52,12 @@ export class Bundler { entryPoints: { [absPath]: absPath, }, - plugins: [MultiAssetPlugin(addToMap)], + plugins: [ + MultiAssetPlugin(addToMap), + NodeResolve({ + extensions: ['.ts', '.js'], + }), + ], }, }; const result = await esbuild.build(options); @@ -85,6 +93,7 @@ export class Bundler { await this.prepare(entry); await this.zip(output); const data = await this.encode(output); + await this.checkSize(output); await this.cleanup(output); return data; } @@ -93,6 +102,16 @@ export class Bundler { return await readFile(outputPath, 'base64'); } + async checkSize(outputPath: string) { + const { size } = await stat(outputPath); + const sizeKb = size / 1024; + if (sizeKb > SIZE_LIMIT_KB) { + throw new Error( + `You have monitors whose size exceeds the ${SIZE_LIMIT_KB}KB limit.` + ); + } + } + async cleanup(outputPath: string) { this.moduleMap = new Map(); await unlink(outputPath); diff --git a/src/push/plugin.ts b/src/push/plugin.ts index 70f6fffd..14cba7b3 100644 --- a/src/push/plugin.ts +++ b/src/push/plugin.ts @@ -23,14 +23,17 @@ * */ -import path from 'path'; +import { isAbsolute, dirname, extname, join } from 'path'; import fs from 'fs/promises'; import * as esbuild from 'esbuild'; +const SOURCE_DIR = join(__dirname, '..', '..'); +const SOURCE_NODE_MODULES = join(SOURCE_DIR, 'node_modules'); + export function commonOptions(): esbuild.BuildOptions { return { bundle: true, - external: ['node_modules', '@elastic/synthetics'], + external: ['@elastic/synthetics'], minify: false, platform: 'node', logLevel: 'silent', @@ -55,27 +58,49 @@ export function MultiAssetPlugin(callback: PluginCallback): esbuild.Plugin { // Note that we use `isAbsolute` to handle UNC/windows style paths like C:\path\to\thing // This is not necessary for relative directories since `.\file` is not supported as an import // nor is `~/path/to/file`. - if (path.isAbsolute(str) || str.startsWith('./') || str.startsWith('../')) { + if (isAbsolute(str) || str.startsWith('./') || str.startsWith('../')) { return true; } return false; }; + // If we're importing the @elastic/synthetics package + // directly from source instead of using the fully + // qualified name, we must skip it too. That's just + // so it doesn't get bundled on tests or when we locally + // refer to the package itself. + const isLocalSynthetics = (entryPath: string) => { + return entryPath.startsWith(SOURCE_DIR); + }; + + // When importing the local synthetics module directly + // it may import its own local dependencies, so we must + // make sure those will be resolved using Node's resolution + // algorithm, as they're still "node_modules" that we must bundle + const isLocalSyntheticsModule = (str: string) => { + return str.startsWith(SOURCE_NODE_MODULES); + }; + return { name: 'esbuild-multiasset-plugin', setup(build) { build.onResolve({ filter: /.*?/ }, async args => { // External and other packages need be marked external to // be removed from the bundle - if ( - build.initialOptions.external?.includes(args.path) || - !isBare(args.path) - ) { + if (build.initialOptions.external?.includes(args.path)) { return { external: true, }; } + if ( + !isBare(args.path) || + args.importer.includes('/node_modules/') || + isLocalSyntheticsModule(args.importer) + ) { + return; + } + if (args.kind === 'entry-point') { return { path: args.path, @@ -86,8 +111,12 @@ export function MultiAssetPlugin(callback: PluginCallback): esbuild.Plugin { // If the modules are resolved locally, then // use the imported path to get full path const entryPath = - path.join(path.dirname(args.importer), args.path) + - path.extname(args.importer); + join(dirname(args.importer), args.path) + extname(args.importer); + + if (isLocalSynthetics(entryPath)) { + return { external: true }; + } + // Spin off another build to copy over the imported modules without bundling const result = await esbuild.build({ ...commonOptions(),