From 8a1ff3a8eb3d39b605e0639825302830b36a87bf Mon Sep 17 00:00:00 2001 From: Maxim Belov Date: Mon, 12 Oct 2026 04:26:52 +0200 Subject: [PATCH] feat: Add support ESBuild builder [skip ci] --- packages/cordova-builders/builders.json | 6 + .../cordova-serve-esbuild/index.ts | 171 ++++++++++++++++++ .../cordova-serve-esbuild/schema.d.ts | 14 ++ .../cordova-serve-esbuild/schema.json | 63 +++++++ .../cordova-builders/cordova-serve/index.ts | 2 +- packages/cordova-builders/package.json | 2 + .../{cordova-serve => utils}/log-server.ts | 0 7 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 packages/cordova-builders/cordova-serve-esbuild/index.ts create mode 100644 packages/cordova-builders/cordova-serve-esbuild/schema.d.ts create mode 100644 packages/cordova-builders/cordova-serve-esbuild/schema.json rename packages/cordova-builders/{cordova-serve => utils}/log-server.ts (100%) diff --git a/packages/cordova-builders/builders.json b/packages/cordova-builders/builders.json index b4dc4a2..6d22eb1 100644 --- a/packages/cordova-builders/builders.json +++ b/packages/cordova-builders/builders.json @@ -11,6 +11,12 @@ "implementation": "./cordova-serve/index", "schema": "./cordova-serve/schema.json", "description": "Run the dev-server with Cordova assets." + }, + + "cordova-serve-esbuild": { + "implementation": "./cordova-serve-esbuild/index", + "schema": "./cordova-serve-esbuild/schema.json", + "description": "Run the esbuild dev-server with Cordova assets." } } } diff --git a/packages/cordova-builders/cordova-serve-esbuild/index.ts b/packages/cordova-builders/cordova-serve-esbuild/index.ts new file mode 100644 index 0000000..485fde1 --- /dev/null +++ b/packages/cordova-builders/cordova-serve-esbuild/index.ts @@ -0,0 +1,171 @@ +import { createBuilder, targetFromTargetString } from '@angular-devkit/architect'; +import type { BuilderContext } from '@angular-devkit/architect'; +import { getSystemPath, join, normalize } from '@angular-devkit/core'; +import { executeDevServerBuilder } from '@angular/build'; +import type { DevServerBuilderOptions, DevServerBuilderOutput } from '@angular/build'; +import type { json } from '@angular-devkit/core'; +import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; +import type * as http from 'http'; +import { extname, resolve } from 'path'; +import { from } from 'rxjs'; +import type { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { GlobalScriptsByBundleName } from '../utils'; +import { augmentIndexHtml } from '../utils/append-scripts'; +import { createConsoleLogServer } from '../utils/log-server'; +import type { CordovaServeBuilderSchema } from './schema'; + +export type CordovaDevServerBuilderOptions = CordovaServeBuilderSchema & json.JsonObject; + +const MIME_TYPES: Record = { + '.js': 'application/javascript', + '.map': 'text/plain', + '.css': 'text/css', + '.html': 'text/html', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +export function serveCordova(options: CordovaServeBuilderSchema, context: BuilderContext): Observable { + const { devServerTarget, port, host, ssl } = options; + const root = context.workspaceRoot; + const devServerTargetSpec = targetFromTargetString(devServerTarget); + + async function setup() { + const devServerTargetOptions = await context.getTargetOptions(devServerTargetSpec); + const devServerName = await context.getBuilderNameForTarget(devServerTargetSpec); + console.log(devServerTargetOptions, devServerName); + + // console.log(devServerTargetOptions); + + devServerTargetOptions.port = port; + devServerTargetOptions.host = host; + devServerTargetOptions.ssl = ssl; + + + // delete devServerTargetOptions.serviceWorker; + + + const formattedOptions = await context.validateOptions(devServerTargetOptions, devServerName); + const serverAssets = prepareEsbuildServerConfig(options, root); + + if (options.consolelogs && options.consolelogsPort) { + await createConsoleLogServer(host, options.consolelogsPort); + } + + return { formattedOptions, serverAssets }; + } + + return from(setup()).pipe( + switchMap(({ formattedOptions, serverAssets }) => + from( + executeDevServerBuilder(formattedOptions as unknown as DevServerBuilderOptions, context as any, { + middleware: buildMiddleware(serverAssets), + indexHtmlTransformer: indexHtmlTransformFactory(serverAssets), + }) + ) + ) + ); +} + +export default createBuilder(serveCordova); + +function buildMiddleware( + serverAssets: EsbuildServerAssets +): ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[] { + const handlers: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[] = []; + + // Serve each script bundle at /{bundleName}.js by concatenating its source files + for (const script of serverAssets.scripts) { + const bundlePath = `/${script.bundleName}.js`; + handlers.push((req, res, next) => { + const reqPath = (req.url ?? '/').split('?')[0]; + if (reqPath === bundlePath) { + const content = script.paths.map((p) => readFileSync(p, 'utf-8')).join('\n'); + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.end(content); + return; + } + next(); + }); + } + + // Serve static asset directories (e.g. cordova platform_www) + for (const dir of serverAssets.staticDirs) { + handlers.push((req, res, next) => { + const urlPath = (req.url ?? '/').split('?')[0]; + const filePath = resolve(dir, urlPath.replace(/^\//, '')); + if (existsSync(filePath) && statSync(filePath).isFile()) { + const contentType = MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream'; + res.writeHead(200, { 'Content-Type': contentType }); + res.end(readFileSync(filePath)); + return; + } + next(); + }); + } + + return handlers; +} + +export const indexHtmlTransformFactory: (serverAssets: EsbuildServerAssets) => (content: string) => Promise = + ({ scripts }) => + (indexHtml: string) => + Promise.resolve(augmentIndexHtml(indexHtml, scripts)); + + +export interface EsbuildServerAssets { + scripts: GlobalScriptsByBundleName[]; + staticDirs: string[]; +} + +export function prepareEsbuildServerConfig(options: CordovaServeBuilderSchema, root: string): EsbuildServerAssets { + const rawScripts: { input: string; bundleName: string }[] = []; + const staticDirs: string[] = []; + const cordovaBasePath = normalize(options.cordovaBasePath ? options.cordovaBasePath : '.'); + + if (options.consolelogs) { + const configPath = getSystemPath(join(normalize(__dirname), '../assets', normalize('consolelog-config.js'))); + writeFileSync( + configPath, + `window.Ionic = window.Ionic || {}; Ionic.ConsoleLogServerConfig = { wsPort: ${options.consolelogsPort} }` + ); + rawScripts.push({ input: configPath, bundleName: 'consolelogs' }); + rawScripts.push({ + input: getSystemPath(join(normalize(__dirname), '../assets', normalize('consolelogs.js'))), + bundleName: 'consolelogs', + }); + } + + if (options.cordovaMock) { + rawScripts.push({ + input: getSystemPath(join(normalize(__dirname), '../assets', normalize('cordova.js'))), + bundleName: 'cordova', + }); + } else if (options.cordovaAssets) { + const platformWWWPath = join(cordovaBasePath, normalize(`platforms/${options.platform}/platform_www`)); + staticDirs.push(getSystemPath(platformWWWPath)); + rawScripts.push({ + input: getSystemPath(join(platformWWWPath, normalize('cordova.js'))), + bundleName: 'cordova', + }); + } + + const scripts = rawScripts.reduce((prev: GlobalScriptsByBundleName[], curr) => { + const resolvedPath = resolve(root, curr.input); + const existingEntry = prev.find((el) => el.bundleName === curr.bundleName); + if (existingEntry) { + existingEntry.paths.push(resolvedPath); + } else { + prev.push({ bundleName: curr.bundleName, inject: true, paths: [resolvedPath] }); + } + return prev; + }, []); + + return { scripts, staticDirs }; +} diff --git a/packages/cordova-builders/cordova-serve-esbuild/schema.d.ts b/packages/cordova-builders/cordova-serve-esbuild/schema.d.ts new file mode 100644 index 0000000..bb7e44d --- /dev/null +++ b/packages/cordova-builders/cordova-serve-esbuild/schema.d.ts @@ -0,0 +1,14 @@ +export interface CordovaServeBuilderSchema { + cordovaBuildTarget: string; + devServerTarget: string; + platform?: string; + port: number; + host: string; + ssl: boolean; + cordovaBasePath?: string; + sourceMap?: boolean; + cordovaAssets?: boolean; + cordovaMock?: boolean; + consolelogs?: boolean; + consolelogsPort?: number; +} diff --git a/packages/cordova-builders/cordova-serve-esbuild/schema.json b/packages/cordova-builders/cordova-serve-esbuild/schema.json new file mode 100644 index 0000000..93bb010 --- /dev/null +++ b/packages/cordova-builders/cordova-serve-esbuild/schema.json @@ -0,0 +1,63 @@ +{ + "title": "Cordova serve for Ionic", + "description": "Options for Cordova serve for Ionic.", + "type": "object", + "properties": { + "cordovaBuildTarget": { + "type": "string", + "description": "Target to use for build." + }, + "consolelogs": { + "type": "boolean", + "description": "Print console logs to the terminal." + }, + "consolelogsPort": { + "type": "number", + "description": "Port for console log server.", + "default": 53703 + }, + "devServerTarget": { + "type": "string", + "description": "Target to use for serve." + }, + "platform": { + "type": "string", + "description": "Cordova platform to use during serve." + }, + "ssl": { + "type": "boolean", + "description": "Serve using HTTPS.", + "default": false + }, + "port": { + "type": "number", + "description": "Port to listen on.", + "default": 4200 + }, + "host": { + "type": "string", + "description": "Host to listen on.", + "default": "localhost" + }, + "cordovaBasePath": { + "type": "string", + "description": "Path to cordova directory" + }, + "sourceMap": { + "type": "boolean", + "description": "Create source-map file" + }, + "cordovaAssets": { + "type": "boolean", + "description": "Bundle Cordova assets with build", + "default": true + }, + "cordovaMock": { + "type": "boolean", + "description": "Bundle empty cordova.js with build", + "default": false + } + }, + "additionalProperties": false, + "required": ["cordovaBuildTarget", "devServerTarget"] +} diff --git a/packages/cordova-builders/cordova-serve/index.ts b/packages/cordova-builders/cordova-serve/index.ts index bab3396..80ad2e7 100644 --- a/packages/cordova-builders/cordova-serve/index.ts +++ b/packages/cordova-builders/cordova-serve/index.ts @@ -19,7 +19,7 @@ import { prepareServerConfig } from '../utils'; import type { FormattedAssets } from '../utils'; import { augmentIndexHtml } from '../utils/append-scripts'; -import { createConsoleLogServer } from './log-server'; +import { createConsoleLogServer } from '../utils/log-server'; import type { CordovaServeBuilderSchema } from './schema'; export type CordovaDevServerBuilderOptions = CordovaServeBuilderSchema & json.JsonObject; diff --git a/packages/cordova-builders/package.json b/packages/cordova-builders/package.json index 3859942..061b5a4 100644 --- a/packages/cordova-builders/package.json +++ b/packages/cordova-builders/package.json @@ -45,6 +45,7 @@ "@angular-devkit/build-angular": ">=21.0.0 <23.0.0", "@angular-devkit/core": ">=21.0.0 <23.0.0", "@angular-devkit/schematics": ">=21.0.0 <23.0.0", + "@angular/build": ">=21.0.0 <23.0.0", "@schematics/angular": ">=21.0.0 <23.0.0" }, "devDependencies": { @@ -52,6 +53,7 @@ "@angular-devkit/build-angular": "^22.0.0", "@angular-devkit/core": "^22.0.0", "@angular-devkit/schematics": "^22.0.0", + "@angular/build": "^22.0.0", "@schematics/angular": "^22.0.0", "@types/node": "^20.19 || ^22.12 || >=24.0.0", "@types/ws": "^8.5.12", diff --git a/packages/cordova-builders/cordova-serve/log-server.ts b/packages/cordova-builders/utils/log-server.ts similarity index 100% rename from packages/cordova-builders/cordova-serve/log-server.ts rename to packages/cordova-builders/utils/log-server.ts