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

idea: bundle embed or static file inside a single file. #3612

Open
wenerme opened this issue Jan 26, 2024 · 3 comments
Open

idea: bundle embed or static file inside a single file. #3612

wenerme opened this issue Jan 26, 2024 · 3 comments

Comments

@wenerme
Copy link

wenerme commented Jan 26, 2024

I want do where to ask about this feature, but I do think this is possible for esbuild.

I want to deliver a single file which can run as a server, that serve the static or dynamic ssr content.
I already use esbuild to bundle everything into a single cli.mjs, but esbuild left other content behind.
I know I can do this by file loader like import content from './index.html', but I think maybe

import efs from './dist' with {type:'dir'}

If this can return a streamich/memfs, maybe a hono server can accept a fs object to use the embeded files, do we can deliver a single file that can provide a functional web app.

Just like //go:embed for golang or include_dir for Rust.

@evanw
Copy link
Owner

evanw commented Jan 26, 2024

You are welcome to write a plugin to do that: https://esbuild.github.io/plugins/

@wenerme
Copy link
Author

wenerme commented Jan 26, 2024

Ok, let me try, this works, but I don't know how to filter by with type:embed, workaround by force a prefix embed:

import fs from 'node:fs/promises';
import path from 'node:path';
import * as esbuild from 'esbuild';
import type { Plugin } from 'esbuild';

let embedPlugin: Plugin = {
  name: 'embed',
  setup(build) {
    build.onResolve({ filter: /^embed:.*/, namespace: 'file' }, (args) => {
      switch (args.kind) {
        case 'import-statement':
        case 'dynamic-import':
          break;
        default:
          return null;
      }

      let p = args.path.slice('embed:'.length);
      if (p.startsWith('.')) {
        p = path.resolve(args.resolveDir, p);
      }
      return {
        path: p,
        namespace: 'embed',
        pluginData: {
          resolveDir: args.resolveDir,
        },
      };
    });

    build.onLoad({ filter: /.*/, namespace: 'embed' }, async (args) => {
      const resolveDir = args.pluginData.resolveDir;
      if (args.with?.type !== 'embed') {
        return null;
      }
      const cwd = args.with.cwd || '/app';
      console.log(`embed ${path.relative(resolveDir, args.path)} to ${cwd}`);
      const stat = await fs.stat(args.path);
      if (!stat.isDirectory()) {
        throw new Error(`Embed need a directory: ${args.path}`);
      }

      async function readDirectory(dir: string, basePath = dir, result: Record<string, any> = {}) {
        const files = await fs.readdir(dir, { withFileTypes: true });

        for (const file of files) {
          const filePath = path.join(dir, file.name);
          const relativePath = path.relative(basePath, filePath);

          if (file.isDirectory()) {
            readDirectory(filePath, basePath, result);
          } else {
            result[path.resolve(cwd, relativePath)] = await fs.readFile(filePath, 'utf8');
          }
        }

        return result;
      }

      const o = await readDirectory(args.path);

      return {
        contents: `
import { memfs } from 'memfs';

const {fs:lfs,vol} = memfs(${JSON.stringify(o)}, '/app/');
const fs = lfs.promises
export {
  fs,vol,lfs
}
        `,
        loader: 'js',
        resolveDir: resolveDir,
      };
    });
  },
};

await esbuild.build({
  entryPoints: ['app.ts'],
  bundle: true,
  outfile: 'out.mjs',
  plugins: [embedPlugin],
  format: 'esm',
  platform: 'node',
  target: 'node20',
  banner: {
    js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);var __filename;var __dirname;{const {fileURLToPath} = await import('url');const {dirname} = await import('path');var __filename = fileURLToPath(import.meta.url); __dirname = dirname(__filename)};`,
  },
});

types.d.ts

declare module 'embed:*' {
  const fs: typeof import('node:fs/promises');
  export { fs };
}

app.ts

import { fs } from './src/utils' with { type: 'embed', cwd: '/app/' };

console.log(`FS`, await fs.readdir('/app'));
console.log(`Content`, await fs.readFile('/app/Closer.ts', 'utf-8'));

@evanw
Copy link
Owner

evanw commented Jan 26, 2024

It's not just you. There isn't a way to declaratively filter by type: 'embed' right now. I still need to add an additional API for that. Right now you have to write code that checks the with map provided to the plugin. It should be the same thing, but just less efficient than it could be if there was a declarative API (since it would avoid some unnecessary IPC traffic).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants