diff --git a/package-lock.json b/package-lock.json index b21afc1..89286bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "workspaces": [ "plugins/plugin-tools", "plugins/vite-plugin", - "plugins/nextjs-plugin" + "plugins/nextjs-plugin", + "plugins/esbuild-plugin" ], "devDependencies": { "@dfinity/eslint-config-oisy-wallet": "^0.2.3", @@ -1700,6 +1701,10 @@ "@junobuild/config": "*" } }, + "node_modules/@junobuild/esbuild-plugin": { + "resolved": "plugins/esbuild-plugin", + "link": true + }, "node_modules/@junobuild/nextjs-plugin": { "resolved": "plugins/nextjs-plugin", "link": true @@ -7681,6 +7686,14 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "plugins/esbuild-plugin": { + "name": "@junobuild/esbuild-plugin", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@junobuild/plugin-tools": "*" + } + }, "plugins/nextjs-plugin": { "name": "@junobuild/nextjs-plugin", "version": "4.5.0", diff --git a/package.json b/package.json index 502a797..8a3699b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "workspaces": [ "plugins/plugin-tools", "plugins/vite-plugin", - "plugins/nextjs-plugin" + "plugins/nextjs-plugin", + "plugins/esbuild-plugin" ], "scripts": { "build": "npm run build --workspaces", diff --git a/plugins/esbuild-plugin/LICENSE b/plugins/esbuild-plugin/LICENSE new file mode 100644 index 0000000..06632c6 --- /dev/null +++ b/plugins/esbuild-plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 David Dal Busco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/esbuild-plugin/README.md b/plugins/esbuild-plugin/README.md new file mode 100644 index 0000000..2db2a90 --- /dev/null +++ b/plugins/esbuild-plugin/README.md @@ -0,0 +1,100 @@ +[![npm][npm-badge]][npm-badge-url] +[![license][npm-license]][npm-license-url] + +[npm-badge]: https://img.shields.io/npm/v/@junobuild/esbuild-plugin +[npm-badge-url]: https://www.npmjs.com/package/@junobuild/esbuild-plugin +[npm-license]: https://img.shields.io/npm/l/@junobuild/esbuild-plugin +[npm-license-url]: https://github.com/junobuild/plugins/blob/main/LICENSE + +# Juno Esbuild Plugin + +An esbuild plugin for [Juno]. + +## Getting started + +The plugin automatically loads your Satellite and Orbiter IDs. + +With these values, you can instantiate Juno in your code without the need to manually define environment variables. + +```javascript +await Promise.all([initSatellite(), initOrbiter()]); +``` + +## Environment variables + +Those following environment variables are injected by this plugin: + +| Environment variable | Value | +| ------------------------- | ------------------------------------------------------------------------ | +| VITE_SATELLITE_ID | Satellite ID from Juno config (per `mode`) | +| VITE_ORBITER_ID | `undefined` in development, Orbiter ID from Juno config. | +| VITE_INTERNET_IDENTITY_ID | `rdmx6-jaaaa-aaaaa-aaadq-cai` | +| VITE_ICP_LEDGER_ID | `ryjl3-tyaaa-aaaaa-aaaba-cai` | +| VITE_ICP_INDEX_ID | `qhbym-qaaaa-aaaaa-aaafq-cai` | +| VITE_NNS_GOVERNANCE_ID | `rrkah-fqaaa-aaaaa-aaaaq-cai` | +| VITE_CMC_ID | `rkp4c-7iaaa-aaaaa-aaaca-cai` | +| VITE_REGISTRY_ID | `rwlgt-iiaaa-aaaaa-aaaaa-cai` | +| VITE_CYCLES_LEDGER_ID | `um5iw-rqaaa-aaaaq-qaaba-cai` | +| VITE_CYCLES_INDEX_ID | `ul4oc-4iaaa-aaaaq-qaabq-cai` | +| VITE_SNS_WASM_ID | `qaa6y-5yaaa-aaaaa-aaafa-cai` | +| VITE_NNS_DAPP_ID | `qoctq-giaaa-aaaaa-aaaea-cai` | +| VITE_CONTAINER | Container URL (emulator or custom); `undefined` by default in production | + +> `VITE_` is the default prefix used by Vite. It can be customized as described in Vite's [documentation](https://vitejs.dev/guide/env-and-mode). + +## Installation + +```bash +npm i @junobuild/esbuild-plugin -D +``` + +## Usage + +```javascript +// vite.config.js +import juno from '@junobuild/esbuild-plugin'; + +export default defineConfig({ + plugins: [juno()] +}); +``` + +## Options + +The plugin can be customized using the optional `juno` configuration object. This allows you to control how the Juno Docker container is used in your project, especially during local development or end-to-end (E2E) testing. + +### `juno.container` + +Use the container option to enable, disable, or fine-tune the use of [Juno Docker](https://github.com/junobuild/juno-docker). + +You can provide: + +- `false` — to disable the container entirely. +- `true` — to enable the container with default settings (only in development mode), which is also the default behavior. +- An object with the following fields: + - `url` (`string`, optional): A custom container URL, including the port. Example: http://127.0.0.1:8000 + - `modes` (`string[]`, optional): An array of modes (e.g., ['development', 'test']) during which the container should be used. + +By default, the container is mounted only in `development` mode. + +```javascript +// vite.config.js +import juno from '@junobuild/esbuild-plugin'; + +export default defineConfig({ + plugins: [ + juno({ + container: { + url: 'http://127.0.0.1:8000', + modes: ['development', 'test'] + } + }) + ] +}); +``` + +## License + +MIT © [David Dal Busco](mailto:david.dalbusco@outlook.com) + +[juno]: https://juno.build diff --git a/plugins/esbuild-plugin/esbuild.mjs b/plugins/esbuild-plugin/esbuild.mjs new file mode 100644 index 0000000..55c6d9e --- /dev/null +++ b/plugins/esbuild-plugin/esbuild.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import {build} from '../../scripts/esbuild.mjs'; + +build(); diff --git a/plugins/esbuild-plugin/package.json b/plugins/esbuild-plugin/package.json new file mode 100644 index 0000000..46f9f2e --- /dev/null +++ b/plugins/esbuild-plugin/package.json @@ -0,0 +1,39 @@ +{ + "name": "@junobuild/esbuild-plugin", + "version": "0.0.1", + "description": "An esbuild plugin for Juno", + "author": "David Dal Busco (https://daviddalbusco.com)", + "license": "MIT", + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "rmdir": "node ../../scripts/rmdir.mjs", + "ts-declaration": "tsc --emitDeclarationOnly --outDir dist", + "build": "tsc --noEmit && npm run rmdir && mkdir -p dist && node esbuild.mjs && npm run ts-declaration" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/junobuild/plugins.git", + "directory": "plugins/esbuild-plugin" + }, + "bugs": { + "url": "https://github.com/junobuild/plugins" + }, + "keywords": [ + "env", + "environment", + "variables", + "esbuild", + "esbuild-plugin" + ], + "homepage": "https://juno.build", + "dependencies": { + "@junobuild/plugin-tools": "*" + } +} diff --git a/plugins/esbuild-plugin/src/index.ts b/plugins/esbuild-plugin/src/index.ts new file mode 100644 index 0000000..492088d --- /dev/null +++ b/plugins/esbuild-plugin/src/index.ts @@ -0,0 +1,98 @@ +import { + type ConfigArgs, + type JunoParams, + JunoPluginError, + initConfig +} from '@junobuild/plugin-tools'; +import type {Plugin, PluginBuild} from 'esbuild'; + +export interface JunoOptions { + emulator?: JunoParams; + envPrefix?: string; +} + +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export default function juno(options?: JunoOptions): Plugin { + return { + name: 'esbuild-plugin-juno', + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + setup(build: PluginBuild) { + const mode = process.env.NODE_ENV; + + const defineConfig = async () => { + const {emulator, envPrefix} = options ?? {}; + + const args: ConfigArgs = {params: emulator, mode: mode ?? 'production'}; + + try { + const {satelliteId, orbiterId, icpIds, container, authClientIds} = await initConfig(args); + + const prefix = `process.env.${envPrefix ?? ''}`; + + const defines = { + [`${prefix}SATELLITE_ID`]: satelliteId, + ...(orbiterId !== undefined && { + [`${prefix}ORBITER_ID`]: orbiterId + }), + ...(icpIds?.internetIdentityId !== undefined && { + [`${prefix}INTERNET_IDENTITY_ID`]: icpIds.internetIdentityId + }), + ...(icpIds?.icpLedgerId !== undefined && { + [`${prefix}ICP_LEDGER_ID`]: icpIds.icpLedgerId + }), + ...(icpIds?.icpIndexId !== undefined && { + [`${prefix}ICP_INDEX_ID`]: icpIds.icpIndexId + }), + ...(icpIds?.nnsGovernanceId !== undefined && { + [`${prefix}NNS_GOVERNANCE_ID`]: icpIds.nnsGovernanceId + }), + ...(icpIds?.cmcId !== undefined && { + [`${prefix}CMC_ID`]: icpIds.cmcId + }), + ...(icpIds?.registryId !== undefined && { + [`${prefix}REGISTRY_ID`]: icpIds.registryId + }), + ...(icpIds?.cyclesLedgerId !== undefined && { + [`${prefix}CYCLES_LEDGER_ID`]: icpIds.cyclesLedgerId + }), + ...(icpIds?.cyclesIndexId !== undefined && { + [`${prefix}CYCLES_INDEX_ID`]: icpIds.cyclesIndexId + }), + ...(icpIds?.snsWasmId !== undefined && { + [`${prefix}SNS_WASM_ID`]: icpIds.snsWasmId + }), + ...(icpIds?.nnsDappId !== undefined && { + [`${prefix}NNS_DAPP_ID`]: icpIds.nnsDappId + }), + ...(container !== undefined && { + [`${prefix}CONTAINER`]: container + }), + ...(authClientIds?.google !== undefined && { + [`${prefix}GOOGLE_CLIENT_ID`]: authClientIds.google + }) + }; + + build.initialOptions.define = { + ...build.initialOptions.define, + ...Object.entries(defines).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: JSON.stringify(value) + }), + {} + ) + }; + } catch (err: unknown) { + if (err instanceof JunoPluginError && mode !== 'production') { + console.warn(err.message); + return; + } + + throw err; + } + }; + + build.onStart(defineConfig); + } + }; +} diff --git a/plugins/esbuild-plugin/src/tests/index.spec.ts b/plugins/esbuild-plugin/src/tests/index.spec.ts new file mode 100644 index 0000000..e3d1347 --- /dev/null +++ b/plugins/esbuild-plugin/src/tests/index.spec.ts @@ -0,0 +1,198 @@ +import * as pluginTools from '@junobuild/plugin-tools'; +import {JunoPluginError} from '@junobuild/plugin-tools'; +import type {PluginBuild} from 'esbuild'; +import juno, {type JunoOptions} from '../index'; + +describe('esbuild-plugin-juno', () => { + let mockBuild: PluginBuild; + + let onStartCallback: (() => Promise) | undefined; + + beforeEach(() => { + vi.clearAllMocks(); + + process.env.NODE_ENV = 'development'; + + onStartCallback = undefined; + + mockBuild = { + initialOptions: { + define: {} + }, + onStart: vi.fn((callback) => { + onStartCallback = callback; + }) + } as unknown as PluginBuild; + }); + + const setupPlugin = (options?: JunoOptions) => { + const plugin = juno(options); + plugin.setup(mockBuild); + + if (onStartCallback === undefined) { + throw new Error('onStartCallback must be defined'); + } + + return onStartCallback; + }; + + it('merges config and sets all define vars', async () => { + const spy = vi.spyOn(pluginTools, 'initConfig').mockResolvedValue({ + satelliteId: 'sat-id', + orbiterId: 'orb-id', + authClientIds: { + google: 'google-client-id-123' + }, + icpIds: { + internetIdentityId: 'ii-id', + icpLedgerId: 'ledger-id', + icpIndexId: 'index-id', + nnsGovernanceId: 'nns-governance-id', + cmcId: 'cmc-id', + registryId: 'registry-id', + cyclesLedgerId: 'cycles-ledger-id', + cyclesIndexId: 'cycles-index-id', + snsWasmId: 'sns-wasm-id', + nnsDappId: 'nns-dapp-id' + }, + container: 'http://localhost:1234' + }); + + const callback = setupPlugin(); + await callback(); + + expect(spy).toHaveBeenCalledWith({ + params: undefined, + mode: 'development' + }); + + expect(mockBuild.initialOptions.define).toEqual({ + 'process.env.SATELLITE_ID': JSON.stringify('sat-id'), + 'process.env.ORBITER_ID': JSON.stringify('orb-id'), + 'process.env.INTERNET_IDENTITY_ID': JSON.stringify('ii-id'), + 'process.env.ICP_LEDGER_ID': JSON.stringify('ledger-id'), + 'process.env.ICP_INDEX_ID': JSON.stringify('index-id'), + 'process.env.NNS_GOVERNANCE_ID': JSON.stringify('nns-governance-id'), + 'process.env.CMC_ID': JSON.stringify('cmc-id'), + 'process.env.REGISTRY_ID': JSON.stringify('registry-id'), + 'process.env.CYCLES_INDEX_ID': JSON.stringify('cycles-index-id'), + 'process.env.CYCLES_LEDGER_ID': JSON.stringify('cycles-ledger-id'), + 'process.env.SNS_WASM_ID': JSON.stringify('sns-wasm-id'), + 'process.env.NNS_DAPP_ID': JSON.stringify('nns-dapp-id'), + 'process.env.CONTAINER': JSON.stringify('http://localhost:1234'), + 'process.env.GOOGLE_CLIENT_ID': JSON.stringify('google-client-id-123') + }); + }); + + it('respects custom env prefix', async () => { + vi.spyOn(pluginTools, 'initConfig').mockResolvedValue({ + satelliteId: 'sat-id', + orbiterId: undefined, + authClientIds: undefined, + icpIds: undefined, + container: undefined + }); + + const callback = setupPlugin({envPrefix: 'TEST_'}); + await callback(); + + expect(mockBuild.initialOptions.define).toEqual({ + 'process.env.TEST_SATELLITE_ID': JSON.stringify('sat-id') + }); + }); + + it('merges with existing define properties', async () => { + mockBuild.initialOptions.define = { + 'process.env.FOO': JSON.stringify('bar') + }; + + vi.spyOn(pluginTools, 'initConfig').mockResolvedValue({ + satelliteId: 'sat-id', + orbiterId: undefined, + authClientIds: undefined, + icpIds: undefined, + container: undefined + }); + + const callback = setupPlugin(); + await callback(); + + expect(mockBuild.initialOptions.define).toEqual({ + 'process.env.FOO': JSON.stringify('bar'), + 'process.env.SATELLITE_ID': JSON.stringify('sat-id') + }); + }); + + it('passes emulator params to initConfig', async () => { + const spy = vi.spyOn(pluginTools, 'initConfig').mockResolvedValue({ + satelliteId: 'sat-id', + orbiterId: undefined, + authClientIds: undefined, + icpIds: undefined, + container: undefined + }); + + const emulatorParams = {container: true}; + + const callback = setupPlugin({emulator: emulatorParams}); + await callback(); + + expect(spy).toHaveBeenCalledWith({ + params: emulatorParams, + mode: 'development' + }); + }); + + it('logs and continues on JunoPluginError in non-production', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(pluginTools, 'initConfig').mockRejectedValue( + new JunoPluginError('Juno config missing') + ); + + const callback = setupPlugin(); + await callback(); + + expect(warn).toHaveBeenCalledWith('Juno config missing'); + expect(mockBuild.initialOptions.define).toEqual({}); + }); + + it('throws JunoPluginError in production', async () => { + process.env.NODE_ENV = 'production'; + + vi.spyOn(pluginTools, 'initConfig').mockRejectedValue( + new JunoPluginError('Juno config missing') + ); + + const callback = setupPlugin(); + + await expect(callback()).rejects.toThrow('Juno config missing'); + }); + + it('throws unknown errors', async () => { + vi.spyOn(pluginTools, 'initConfig').mockRejectedValue(new Error('Boom')); + + const callback = setupPlugin(); + + await expect(callback()).rejects.toThrow('Boom'); + }); + + it('defaults to production mode when NODE_ENV is not set', async () => { + delete process.env.NODE_ENV; + + const spy = vi.spyOn(pluginTools, 'initConfig').mockResolvedValue({ + satelliteId: 'sat-id', + orbiterId: undefined, + authClientIds: undefined, + icpIds: undefined, + container: undefined + }); + + const callback = setupPlugin(); + await callback(); + + expect(spy).toHaveBeenCalledWith({ + params: undefined, + mode: 'production' + }); + }); +}); diff --git a/plugins/esbuild-plugin/src/tests/tsconfig.json b/plugins/esbuild-plugin/src/tests/tsconfig.json new file mode 100644 index 0000000..e7f236e --- /dev/null +++ b/plugins/esbuild-plugin/src/tests/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../tsconfig.spec.json" +} diff --git a/plugins/esbuild-plugin/tsconfig.json b/plugins/esbuild-plugin/tsconfig.json new file mode 100644 index 0000000..4082f16 --- /dev/null +++ b/plugins/esbuild-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +}