Vitejs/Rollupjs/Esbuild: bundle content scripts for chrome extension - google-chrome-extension

How to correctly bundle content scripts for a chrome extension using Vite.js? Content scripts do not support ES modules (compared to background service worker), so every content script javascript file should not have any import/export statements, all libraries and other external imports should be inlined in every content script as a single file.
I'm currently using the following vite configuration, where I programmatically pass all my content script paths as the entry.
vite.config.ts
export const extensionScriptsConfig = (
entry: string[],
outDir: string,
mode: Env,
): InlineConfig => {
const config = defineConfig({
root: __dirname,
mode,
build: {
outDir,
emptyOutDir: false,
minify: false,
copyPublicDir: false,
target: 'esnext',
lib: {
entry,
formats: ['es'],
fileName: (_, entry) => entry + '.js',
},
},
});
return { configFile: false, ...config };
};
entry contains the following paths
Directory: C:\Users\code\extension\src\content-scripts
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 15/01/2023 14:11 1135 inject-iframe.ts
-a--- 15/01/2023 14:11 2858 inject.ts
-a--- 14/01/2023 17:49 223 tsconfig.json
The setup above works correctly - each content script is a separate file with no import.
But when I try to import a node_module into inject-iframe.ts and inject.ts the following happens in the dist folder:
dist\src\content-scripts\inject.js
import { m as mitt } from './mitt-eb023923.mjs';
...
dist\src\content-scripts\inject-iframe.js
import { m as mitt } from './mitt-eb023923.mjs';
...
The dist:
Directory: C:\Users\code\extension\dist\src\content-scripts
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 15/01/2023 14:44 1093 inject-iframe.js
-a--- 15/01/2023 14:44 2300 inject.js
-a--- 15/01/2023 14:44 334 mitt-eb023923.mjs
And since ES modules are not supported in the content script context the above doesn't work.
I am also not sure if it should be configured via vite, esbuild or rollup options and if I should use target: 'esnext', and formats: ['es'], for my content scripts vite config.
I target manifest v3 extension and the recent chrome version, so older browsers support is not important.
And as seen in the snippets above I also need to import some node_modules into my content scripts.

Related

How to compile a local package with tsc?

Project Structure
I have a monorepo (using npm workspaces) that contains a directory api (an express API written in typescript). api uses a local package #myapp/server-lib (typescript code).
The directory structure is:
.
├── api/
└── libs/
   └── server-lib/
Problem
When I build api using tsc, the build output contains require statements for #myapp/server-lib (the server-lib package). However, when the API is deployed the server can't resolve #myapp/server-lib (since its not meant to be installed from the npm registry).
How can I get tsc to compile #myapp/server-lib removing require statements for #myapp/server-lib in the built code and replacing it with references to the code that was being imported?
The behavior I am looking to achieve is what next-transpile-modules does for Next.js.
I tried to use typescript project references, that did not compile the imported #myapp/server-lib. I also read up on why I didn't encounter this issue in my NextJS front-end (also housed in the same monorepo, relying on a different but very similar local package) and that is how I landed on next-transpile-modules.
Would appreciate any help or tips in general on how to build a typescript project that uses a local package. Thank You!!
UPDATE (12/28/2022)
I solved this by using esbuild to build api into a single out.js file. This includes all dependencies (therefore #myapp/server-lib.
The overall build process now looks like:
npx tsc --noEmit # checks types but does not output files
node build.js # uses esbuild to build the project
Where the build.js script is:
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
setup(build) {
// If a ".node" file is imported within a module in the "file" namespace, resolve
// it to an absolute path and put it into the "node-file" virtual namespace.
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
path: require.resolve(args.path, { paths: [args.resolveDir] }),
namespace: 'node-file',
}))
// Files in the "node-file" virtual namespace call "require()" on the
// path from esbuild of the ".node" file in the output directory.
build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
contents: `
import path from ${JSON.stringify(args.path)}
try { module.exports = require(path) }
catch {}
`,
}))
// If a ".node" file is imported within a module in the "node-file" namespace, put
// it in the "file" namespace where esbuild's default loading behavior will handle
// it. It is already an absolute path since we resolved it to one above.
build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
path: args.path,
namespace: 'file',
}))
// Tell esbuild's default loading behavior to use the "file" loader for
// these ".node" files.
let opts = build.initialOptions
opts.loader = opts.loader || {}
opts.loader['.node'] = 'file'
},
}
require("esbuild").build({
entryPoints: ["./src/server.ts"], // the entrypoint of the server
platform: "node",
target: "node16.0",
outfile: "./build/out.js", // the single file it will bundle everything into
bundle: true,
loader: {".ts": "ts"},
plugins: [nativeNodeModulesPlugin], // addresses native node modules (like fs)
})
.then((res) => console.log(`⚡ Bundled!`))
.catch(() => process.exit(1));
Solved (12/28/2022)
I solved this by using esbuild to build api into a single out.js file. This includes all dependencies (therefore #myapp/server-lib.
The overall build process now looks like:
npx tsc --noEmit # checks types but does not output files
node build.js # uses esbuild to build the project
Where the build.js script is:
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
setup(build) {
// If a ".node" file is imported within a module in the "file" namespace, resolve
// it to an absolute path and put it into the "node-file" virtual namespace.
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
path: require.resolve(args.path, { paths: [args.resolveDir] }),
namespace: 'node-file',
}))
// Files in the "node-file" virtual namespace call "require()" on the
// path from esbuild of the ".node" file in the output directory.
build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
contents: `
import path from ${JSON.stringify(args.path)}
try { module.exports = require(path) }
catch {}
`,
}))
// If a ".node" file is imported within a module in the "node-file" namespace, put
// it in the "file" namespace where esbuild's default loading behavior will handle
// it. It is already an absolute path since we resolved it to one above.
build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
path: args.path,
namespace: 'file',
}))
// Tell esbuild's default loading behavior to use the "file" loader for
// these ".node" files.
let opts = build.initialOptions
opts.loader = opts.loader || {}
opts.loader['.node'] = 'file'
},
}
require("esbuild").build({
entryPoints: ["./src/server.ts"], // the entrypoint of the server
platform: "node",
target: "node16.0",
outfile: "./build/out.js", // the single file it will bundle everything into
bundle: true,
loader: {".ts": "ts"},
plugins: [nativeNodeModulesPlugin], // addresses native node modules (like fs)
})
.then((res) => console.log(`⚡ Bundled!`))
.catch(() => process.exit(1));
On my server, the start script in package.json is just node out.js and there are no dependencies or devDependencies since all are bundled into out.js.

Vite: Including files in build output

This is a Vue 3 + Vuetify + TS + Vite + VSCode project.
I'm trying to bundle an XML file in the production build. Some transformation needs to be applied on the file before spitting it out. Found this Vite plug-in that can do transformations. But unfortunately, it doesn't seem to touch XML files in any way. If I put my XML file in public folder, it gets copied to the build output, but is not processed by the transformation plugin. If I put it in assets or somewhere else under src, it is simply ignored.
How can I ask Vite to include certain file(s) in the build output and pass it through transformation?
Note: Before I migrated the project to Vite, I was using Vue 2 and WebPack, where I could use the well-known CopyWebpackPlugin to perform this transformation. Haven't been able to find locate its Vite equivalent till now.
You may want to just write a script to do the transformation and add it to your npm scripts. I created a simple chrome extension to play around with VITE. Having multiple html files was pretty simple:
import { defineConfig, BuildOptions } from 'vite'
import vue from '#vitejs/plugin-vue'
const { resolve } = require('path')
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
popup: resolve(__dirname, 'popup/index.html'),
options: resolve(__dirname, 'options/index.html'),
},
}
}
})
But I had to create a separate vite config file to process the background script since it had special configuration (didn't want hashing so I could specify the name in my manifest, esm module format), and it takes the typescript and outputs 'background.js' in the public folder:
import { defineConfig } from 'vite'
const { resolve } = require('path')
// https://vitejs.dev/config/
export default defineConfig({
build: {
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, 'background.ts'),
output: {
format: "esm",
file: "public/background.js",
dir: null,
}
}
}
})
You could simply have the xml file in your src folder and run a special script (create a 'scripts' folder maybe) to do the transform and store the result in the public folder where vite will pick it up and copy it to the dist folder. Your 'build' script in package.json could look something like this:
"scripts": {
"build": "node scripts/transform-xml.mjs && vite build",
},
Author of the package has introduced a new option named replaceFiles in the version 2.0.1 using which you can specify the files that will be passed through the transform pipeline. I can now do the following in my vite.config.js to replace variables in my output manifest.xml file after build:
const replaceFiles = [resolve(join(__dirname, '/dist/manifest.xml'))];
return defineConfig({
...
plugins: [
vue(),
transformPlugin({
replaceFiles,
replace: {
VERSION_NUMBER: process.env.VITE_APP_VERSION,
SERVER_URL: process.env.VITE_SERVER_URL,
},
...
}),
...
});

Vite library with Windicss

I am using Vite (Vue3) with Windi CSS to develop a library. I am using library mode for the build (https://vitejs.dev/guide/build.html#library-mode) with the following config:
vite.config.js
export default defineConfig({
plugins: [vue(), WindiCSS()],
build: {
lib: {
entry: path.resolve(__dirname, 'src/lib.js'),
name: 'MyLIB',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
});
My entry file (src/lib.js) only includes a few Vue components in it and looks like this:
lib.js
export { default as AButton } from './components/AButton/AButton.vue';
export { default as ACheckbox } from './components/ACheckbox/ACheckbox.vue';
import 'virtual:windi.css';
import './assets/fonts.css';
When I build the library I get the js for just those components but the css is for every Vue file in the src folder and not only the ones i included in my lib.js file. I know the default behavior for Windi CSS is to scan the whole src folder but in this case, I only want it to scan the components I added to my entry.
Any ideas?
You should be able to restrict the scan by using extract.include and extract.exclude options, see there : https://windicss.org/guide/extractions.html#scanning
From the doc
If you want to enable/disable scanning for other file-types or locations, you can configure it using include and exclude options

compile typescript to js with webpack and keep directory structure

I want webpack to compile my typescript node project into js but I want it to maintain the directory structure and not bundle into 1 file.
Is this possible?
My structure is:
src
|_controllers
|_home
|_index.ts
|_ services
// etc.
And I want it to compile to:
dist
|_controllers
|_home
|_index.ts
|_ services
// etc.
currently my config is like this:
{
name: 'api',
target: 'node',
externals: getExternals(),
entry: isDevelopment ? [...entries] : entries,
devtool: !isDevelopment && 'cheap-module-source-map',
output: {
path: paths.appBuild,
filename: '[name].js',
libraryTarget: 'commonjs2'
},
plugins: [
new WriteFilePlugin(),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
}),
isProduction && new webpack.optimize.ModuleConcatenationPlugin()
]
}
Is it possible with webpack?
I can't use just tsc because I have a yarn workspaces monorepo and I might have a link reference like this:
import {something} from '#my/package';
#my/package does not exist in npm and only exists in the context of the monorepo, I can use node externals with webpack to include it in the bundle I don't think I can keep the folder structure this way.
Would the new typescript 3.0 project references solve this problem?
Understood your concerns. Looks it's completely doable by transpile-webpack-plugin. You may setup it as below:
const TranspilePlugin = require('transpile-webpack-plugin');
module.exports = {
// ...
entry: './src/controllers/home/index.ts',
output: {
path: __dirname + '/dist',
},
plugins: [new TranspilePlugin({ longestCommonDir: './src' })],
};
The plugin works well with webpack resolve.alias and externals. All the files directly or indirectly imported by the entry will be collected and compiled into the output dir seperately without bundling but with keeping the directory structure and file names. Just have a try. 🙂

requirejs optimizer doesn't generate '.js' version of the text resource

In my project I use yeoman (1.0.6). In a fresh webapp copy installed requirejs-text plugin to include template.html.
main.js
require.config({
paths: {
jquery: '../bower_components/jquery/jquery',
text: '../bower_components/requirejs-text/text'
}
});
require(['jquery', 'text!../templates.html'], function ($, templates) {
....
After building and optimizing a whole project, I expect to have generated templates.js file instead of templates.html ( added
"optimizeAllPluginResources: true" as described here )
Gruntfile.js ( won't paste all code, just optimization settings )
....
requirejs: {
dist: {
options: {
baseUrl: '<%= yeoman.app %>/scripts',
optimize: 'none',
optimizeAllPluginResources: true,
preserveLicenseComments: false,
useStrict: true,
wrap: true
}
}
},
....
After grunt 'build' task is completed I see that template.html content is in main.js and there is no generated templates.js file
After adding (also have to set in copy task to copy requirejs-text plugin form app to dir folder ):
stubModules: ['text'],
exclude: ['text!../templates.html'],
files are excluded as expected, but there is still no templates.js file. ( get an error as expected: XMLHttpRequest cannot load file:///...../dist/templates.html. Cross origin requests are only supported for HTTP. it works fine with local HTTP )
My question is: What settings am I missing to generate templates.js file with a requirejs optimizer?
p.s. googled, spent all day, tried more than wrote here.
Thank You in Advance

Resources