Skip to content

feat: handle imported files other then ts,js #1004

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion __tests__/push/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
*/

import * as esbuild from 'esbuild';
import { mkdir, rm, writeFile } from 'fs/promises';
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import AdmZip from 'adm-zip';
import { commonOptions } from '../../src/core/transform';
import { SyntheticsBundlePlugin } from '../../src/push/plugin';
import { Bundler } from '../../src/push/bundler';

describe('SyntheticsBundlePlugin', () => {
const PROJECT_DIR = join(__dirname, 'test-bundler');
Expand Down Expand Up @@ -62,3 +64,126 @@ journey('journey 1', () => {
expect(result.outputFiles[0].text).toMatchSnapshot();
});
});

describe('Asset Plugin', () => {
const PROJECT_DIR = join(__dirname, 'test-assets');
const journeyFile = join(PROJECT_DIR, 'bundle.journey.ts');
const assetFile = join(PROJECT_DIR, 'sample.pdf');
const zipOutput = join(PROJECT_DIR, 'output.zip');
const csvFile = join(PROJECT_DIR, 'sample.csv');
const assetContent = 'This is a test PDF file';

beforeAll(async () => {
await mkdir(PROJECT_DIR, { recursive: true });

// Create a sample asset file
await writeFile(assetFile, assetContent);

// Create a journey file that imports the asset
await writeFile(
journeyFile,
`import pdfPath from './sample.pdf';
console.log("PDF file path:", pdfPath);`
);
});

afterAll(async () => {
await rm(PROJECT_DIR, { recursive: true, force: true });
});

it('should include imported assets in the bundle', async () => {
const assets: { [key: string]: Uint8Array } = {};

const assetPlugin: esbuild.Plugin = {
name: 'asset-plugin',
setup(build) {
build.onLoad({ filter: /\.(pdf|xlsx?|csv)$/ }, async args => {
assets[args.path] = await readFile(args.path);
return {
contents: `export default ${JSON.stringify(args.path)};`, // Keep path reference
loader: 'text',
};
});
},
};

const result = await esbuild.build({
bundle: true,
sourcemap: false,
write: false,
entryPoints: [journeyFile],
plugins: [assetPlugin],
});

expect(result.outputFiles).toBeDefined();
expect(Object.keys(assets)).toContain(assetFile);

// Ensure the asset is referenced in the output
const bundleText = result.outputFiles[0].text;
expect(bundleText).toContain('push/test-assets/sample.pdf');
});

it('should bundle, zip, and contain the correct pdf asset content', async () => {
const bundler = new Bundler();
// Build & zip
const base64Zip = await bundler.build(journeyFile, zipOutput);
await mkdir(PROJECT_DIR + '/zip/', { recursive: true });

const zipPath = join(PROJECT_DIR, '/zip/test-output.zip');

// Convert Base64 back to a ZIP file
await writeFile(zipPath, Buffer.from(base64Zip, 'base64'));

// Read the generated ZIP file
const zip = new AdmZip(zipPath);
const zipEntries = zip.getEntries().map(entry => entry.entryName);

// Check that the asset is included in the ZIP archive
expect(zipEntries).toEqual([
'__tests__/push/test-assets/bundle.journey.ts',
'__tests__/push/test-assets/sample.pdf',
]);

// Extract and verify asset content
const extractedAsset = zip.readAsText(
'__tests__/push/test-assets/sample.pdf'
);
expect(extractedAsset).toBe(assetContent);
});

it('should bundle, zip, and contain the correct csv asset content', async () => {
await writeFile(
journeyFile,
`
import sampleCsv from './sample.csv';
console.log("CSV file path:", sampleCsv);
`
);
await writeFile(csvFile, `id,name\n1,Test User\n2,Another User`);
const bundler = new Bundler();
// Build & zip
const base64Zip = await bundler.build(journeyFile, zipOutput);
await mkdir(PROJECT_DIR + '/zip/', { recursive: true });

const zipPath = join(PROJECT_DIR, '/zip/test-output.zip');

// Convert Base64 back to a ZIP file
await writeFile(zipPath, Buffer.from(base64Zip, 'base64'));

// Read the generated ZIP file
const zip = new AdmZip(zipPath);
const zipEntries = zip.getEntries().map(entry => entry.entryName);

// Check that the asset is included in the ZIP archive
expect(zipEntries).toEqual([
'__tests__/push/test-assets/bundle.journey.ts',
'__tests__/push/test-assets/sample.csv',
]);

// Extract and verify asset content
const extractedAsset = zip.readAsText(
'__tests__/push/test-assets/sample.csv'
);
expect(extractedAsset).toBe(`id,name\n1,Test User\n2,Another User`);
});
});
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@types/stack-utils": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"adm-zip": "^0.5.16",
"eslint": "^8.23.1",
"husky": "^4.3.6",
"is-positive": "3.1.0",
Expand Down
35 changes: 32 additions & 3 deletions src/core/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ sourceMapSupport.install({
},
});

const TEXT_EXTS = [
'.csv',
'.pdf',
'.docx',
'.odt',
'.xlsx',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.zip',
'.tar.gz',
'.rar',
'.7z',
'.txt',
'.log',
'.json',
'.xml',
'.mp3',
'.wav',
'.mp4',
'.avi',
'.webm',
];

/**
* Default list of files and corresponding loaders we support
* while pushing project based monitors
Expand All @@ -71,6 +97,7 @@ const LOADERS: Record<string, Loader> = {

const getLoader = (filename: string) => {
const ext = path.extname(filename);
if (TEXT_EXTS.includes(ext)) return 'text';
return LOADERS[ext] || 'default';
};

Expand All @@ -92,7 +119,7 @@ export function commonOptions(): CommonOptions {

/**
* Transform the given code using esbuild and save the corresponding
* map file in memory to be retrived later.
* map file in memory to be retried later.
*/
export function transform(
code: string,
Expand All @@ -105,7 +132,7 @@ export function transform(
loader: getLoader(filename),
/**
* Add this only for the transformation phase, using it on
* bundling phase would disable tree shaking and uncessary bloat
* bundling phase would disable tree shaking and unnecessary bloat
*
* Ensures backwards compatability with tsc's implicit strict behaviour
*/
Expand Down Expand Up @@ -148,7 +175,9 @@ export function installTransform() {
const { code } = transform(source, filename);
return code;
},
{ exts: ['.ts', '.js', '.mjs', '.cjs'] }
{
exts: ['.ts', '.js', '.mjs', '.cjs', ...TEXT_EXTS],
} // List of file extensions to hook
);

return () => {
Expand Down
47 changes: 41 additions & 6 deletions src/push/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/

import path from 'path';
import { unlink, readFile } from 'fs/promises';
import { readFile, unlink } from 'fs/promises';
import { createWriteStream } from 'fs';
import * as esbuild from 'esbuild';
import archiver from 'archiver';
Expand All @@ -37,6 +37,26 @@ function relativeToCwd(entry: string) {

export class Bundler {
async bundle(absPath: string) {
const assets: { [key: string]: Uint8Array } = {}; // Store asset files
const assetPlugin: esbuild.Plugin = {
name: 'asset-plugin',
setup(build) {
build.onLoad(
{
filter:
/\.(pdf|docx?|odt|csv|xlsx?|png|jpe?g|gif|webp|zip|tar\.gz|rar|7z|txt|log|json|xml|mp3|wav|mp4|avi|webm)$/,
},
async args => {
const content = await readFile(args.path, 'utf-8'); // Read file contents as text
assets[args.path] = await readFile(args.path);
return {
contents: `export default ${JSON.stringify(content)};`,
loader: 'text',
};
}
);
},
};
const options: esbuild.BuildOptions = {
...commonOptions(),
...{
Expand All @@ -46,17 +66,22 @@ export class Bundler {
minifyWhitespace: true,
sourcemap: 'inline',
external: ['@elastic/synthetics'],
plugins: [SyntheticsBundlePlugin()],
plugins: [SyntheticsBundlePlugin(), assetPlugin],
},
};
const result = await esbuild.build(options);
if (result.errors.length > 0) {
throw result.errors;
}
return result.outputFiles[0].text;
return { code: result.outputFiles[0].text, assets };
}

async zip(source: string, code: string, dest: string) {
async zip(
source: string,
code: string,
dest: string,
assets: { [key: string]: Uint8Array }
) {
return new Promise((fulfill, reject) => {
const output = createWriteStream(dest);
const archive = archiver('zip', {
Expand All @@ -72,13 +97,23 @@ export class Bundler {
name: relativePath,
date: new Date('1970-01-01'),
});

// Add asset files
for (const [filePath, content] of Object.entries(assets)) {
const assetRelativePath = relativeToCwd(filePath);
archive.append(content, {
name: assetRelativePath,
date: new Date('1970-01-01'),
});
}

archive.finalize();
});
}

async build(entry: string, output: string) {
const code = await this.bundle(entry);
await this.zip(entry, code, output);
const { code, assets } = await this.bundle(entry);
await this.zip(entry, code, output, assets);
const content = await this.encode(output);
await this.cleanup(output);
return content;
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"skipLibCheck": true,
"moduleResolution": "Node"
},
"include": ["src/**/*"]
"include": ["src/**/*", "src/types/*.d.ts"]
}
Loading