From c271e645b7941cb61136e050954a2d245bdeebbc Mon Sep 17 00:00:00 2001 From: RasputinKaiser <178525839+RasputinKaiser@users.noreply.github.com> Date: Thu, 14 May 2026 06:06:53 -0400 Subject: [PATCH] Make browser-edge builds path-safe --- .../targets/browser-edge/src/index.test.ts | 86 ++++++++++++++++++- packages/targets/browser-edge/src/index.ts | 60 ++++++++++--- 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/packages/targets/browser-edge/src/index.test.ts b/packages/targets/browser-edge/src/index.test.ts index a556a6ca..a43c781d 100644 --- a/packages/targets/browser-edge/src/index.test.ts +++ b/packages/targets/browser-edge/src/index.test.ts @@ -1,4 +1,88 @@ -import { smokeTest } from '@profullstack/sh1pt-core/testing'; +import { fakeBuildContext, smokeTest } from '@profullstack/sh1pt-core/testing'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { execMock } = vi.hoisted(() => ({ + execMock: vi.fn(), +})); + +vi.mock('@profullstack/sh1pt-core', async () => ({ + ...await vi.importActual('@profullstack/sh1pt-core'), + exec: execMock, +})); + import adapter from './index.js'; smokeTest(adapter, { idPrefix: 'browser', requireKind: true }); + +const tempDirs: string[] = []; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe('browser-edge target adapter', () => { + it('writes a package plan without touching the source directory in dry-run builds', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-out-')); + const projectDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-project-')); + tempDirs.push(outDir, projectDir); + + const result = await adapter.build(fakeBuildContext({ + outDir, + projectDir, + version: '1.2.3', + dryRun: true, + }) as any, { + productId: 'edge-product', + sourceDir: 'extension-dist', + }); + + expect(execMock).not.toHaveBeenCalled(); + expect(result.artifact).toBe(join(outDir, 'edge-package.json')); + const plan = JSON.parse(await readFile(result.artifact, 'utf-8')); + expect(plan).toEqual({ + provider: 'microsoft-edge-addons', + productId: 'edge-product', + version: '1.2.3', + sourceDir: join(projectDir, 'extension-dist'), + artifact: join(outDir, 'edge-product-1.2.3.zip'), + command: ['zip', '-r', join(outDir, 'edge-product-1.2.3.zip'), '.'], + cwd: join(projectDir, 'extension-dist'), + }); + }); + + it('packages the project-relative source directory with zip for real builds', async () => { + execMock.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' }); + + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-out-')); + const projectDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-project-')); + const sourceDir = join(projectDir, 'dist'); + tempDirs.push(outDir, projectDir); + await mkdir(sourceDir, { recursive: true }); + await writeFile(join(sourceDir, 'manifest.json'), JSON.stringify({ manifest_version: 3 }), 'utf-8'); + + const ctx = fakeBuildContext({ + outDir, + projectDir, + version: '1.2.3', + dryRun: false, + }); + const result = await adapter.build(ctx as any, { + productId: 'edge-product', + }); + + const artifact = join(outDir, 'edge-product-1.2.3.zip'); + expect(execMock).toHaveBeenCalledWith('zip', ['-r', artifact, '.'], { + cwd: sourceDir, + log: ctx.log, + throwOnNonZero: true, + }); + expect(result).toEqual({ artifact }); + }); +}); diff --git a/packages/targets/browser-edge/src/index.ts b/packages/targets/browser-edge/src/index.ts index b24edfc0..bc4ed283 100644 --- a/packages/targets/browser-edge/src/index.ts +++ b/packages/targets/browser-edge/src/index.ts @@ -1,6 +1,6 @@ -import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; -import { execSync } from 'node:child_process'; -import { readFileSync, existsSync } from 'node:fs'; +import { defineTarget, exec, manualSetup } from '@profullstack/sh1pt-core'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { isAbsolute, join } from 'node:path'; interface Config { productId: string; // Edge Partner Center product ID @@ -8,29 +8,65 @@ interface Config { notes?: string; // release notes for reviewer } +function sourceDir(ctx: { projectDir: string }, config: Config): string { + const dir = config.sourceDir ?? 'dist'; + return isAbsolute(dir) ? dir : join(ctx.projectDir, dir); +} + +function packageArtifact(ctx: { outDir: string; version: string }, config: Config): string { + return join(ctx.outDir, `${config.productId}-${ctx.version}.zip`); +} + +function packagePlan(ctx: { projectDir: string; outDir: string; version: string }, config: Config) { + const src = sourceDir(ctx, config); + const artifact = packageArtifact(ctx, config); + return { + provider: 'microsoft-edge-addons', + productId: config.productId, + version: ctx.version, + sourceDir: src, + artifact, + command: ['zip', '-r', artifact, '.'], + cwd: src, + }; +} + export default defineTarget({ id: 'browser-edge', kind: 'browser-ext', label: 'Microsoft Edge Add-ons', async build(ctx, config) { - const src = config.sourceDir ?? 'dist/'; - const zipPath = `${ctx.outDir}/${config.productId}-${ctx.version}.zip`; + const src = sourceDir(ctx, config); + const zipPath = packageArtifact(ctx, config); ctx.log(`pack Edge extension from ${src} for v${ctx.version}`); + if (ctx.dryRun) { + const planPath = join(ctx.outDir, 'edge-package.json'); + await mkdir(ctx.outDir, { recursive: true }); + await writeFile(planPath, `${JSON.stringify(packagePlan(ctx, config), null, 2)}\n`, 'utf-8'); + return { artifact: planPath }; + } + // Validate manifest.json exists and is manifest_version 3 - const manifestPath = `${src}/manifest.json`; - if (!existsSync(manifestPath)) { + const manifestPath = join(src, 'manifest.json'); + let manifestText: string; + try { + manifestText = await readFile(manifestPath, 'utf-8'); + } catch { throw new Error(`manifest.json not found at ${manifestPath} — run a build step first`); } - const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')); + const manifest = JSON.parse(manifestText) as { manifest_version?: number }; if (manifest.manifest_version !== 3) { ctx.log(`manifest_version is ${manifest.manifest_version}, Edge requires v3`, 'warn'); } - // Zip the extension directory - execSync(`mkdir -p ${JSON.stringify(ctx.outDir)}`, { stdio: 'ignore' }); - execSync(`cd ${JSON.stringify(src)} && zip -r ${JSON.stringify(zipPath)} .`, { stdio: 'pipe' }); + await mkdir(ctx.outDir, { recursive: true }); + await exec('zip', ['-r', zipPath, '.'], { + cwd: src, + log: ctx.log, + throwOnNonZero: true, + }); ctx.log(`created ${zipPath}`); return { artifact: zipPath }; @@ -77,7 +113,7 @@ export default defineTarget({ // Step 2: Upload the package (zip) as a draft submission ctx.log('uploading package...'); const uploadUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${config.productId}/submissions/draft/package`; - const zipBuf = readFileSync(ctx.artifact); + const zipBuf = await readFile(ctx.artifact); const uploadRes = await fetch(uploadUrl, { method: 'PUT',