Engineering Journal
Ginexys
Ginexys

Marketplace Launch Postmortem

2026-05-30

Shipping five VS Code extensions to Marketplace in one session

TLDR

I expected vsce publish to be a 30 minute task. It took five hours. The work wasn't writing READMEs or LICENSE files. The work was discovering that pnpm workspaces and the VS Code Marketplace operate on incompatible assumptions about what a package is, and refactoring code I thought was production-ready to fix both.

What I thought publish day would look like

Run npm i -g @vscode/vsce. Generate a PAT. Run vsce login ginexys. Run vsce publish in each of five package directories. Ship.

The extensions had been F5-tested for weeks. Compiled cleanly via pnpm -r compile. Loaded correctly in the Extension Development Host. The tools rendered. The auth flow worked. I had a publisher account, a domain, a logo. What could go wrong.

What actually happened

vsce package in the core directory produced a 55 MB .vsix containing every sibling package, the workspace root package.json, pnpm-workspace.yaml, the .vscode/ directory, and the entire build scripts folder. The existing .vscodeignore excluded node_modules/ at depth 1 but pnpm's symlinked dependency graph traversed up and back down through node_modules/.pnpm/ and pulled in everything.

I added explicit ../core/, ../tafne/, etc. patterns plus ../../ and ../../.vscode/*. That worked. Core dropped to 216 KB.

Then I noticed something worse. The compiled tafne/out/editor/TafneEditorProvider.js contained require("core/webview/html-rewriter"). The line in source was import { rewriteHtmlForWebview } from 'core/webview/html-rewriter';. In dev this resolved through tafne/node_modules/core, a pnpm symlink pointing at the sibling package. In production it would fail. VS Code installs each extension into a separate sandboxed directory with no shared node_modules. The symlink doesn't exist post-install.

The fix was a full refactor. Core's activate() function had to return a public API object. Tafne and schema had to call into that object at runtime through vscode.extensions.getExtension('ginexys.ginexys-core').exports. The import statement had to go. The TypeScript types had to be inlined as local declarations because the runtime resolution doesn't carry types across extension boundaries.

I rewrote three files. Recompiled. Repackaged. Then vsce publish rejected name: "core" because someone else owns it on Marketplace globally. The name field is not scoped to publisher. Renamed to ginexys-core. sed-replaced 11 source files. Recompiled. Repackaged.

The first actual publish attempt timed out at the Marketplace API. Not a code error. Just slow. I retried. It succeeded.

What I threw away

The assumption that pnpm workspaces and Marketplace publishing share a coherent model of "a package."

In pnpm a package is a folder with a package.json that participates in a workspace graph. Symlinks make sibling code feel like one project. You import freely across packages.

In Marketplace a package is a .vsix file containing exactly the bytes the user will install. There is no graph. There are no siblings. There are only extensionDependencies, which guarantee install order but not code sharing.

The bridge between these models is the activate-exports pattern. Core's activate returns whatever satellites need. Satellites resolve at runtime. The workspace can keep using symlinks for dev convenience because the symlinks are no longer load-bearing.

I also threw away the assumption that "name fields are unique within my publisher." They are not. They are globally unique across the entire Marketplace. Anyone in the world can take a name out from under you.

What survived

All the actual functionality. The webview HTML rewriter logic was right. The auth flow was right. The IPC router was right. The PDF extraction pipeline was right. The TAFNE table editor was right. The Schema editor was right.

The shape of the deployment changed. The contents did not.

Lesson

Test publish prep on day one of the project, not the day you want to ship.

If I had run vsce package even once during development, both bugs would have surfaced immediately. The symlink leak would have been obvious. The cross-extension require would have failed in the first install I tried on a clean VS Code. Instead I discovered both five minutes before I expected to ship.

Build the deploy pipeline before you need it. Run it occasionally. Don't trust that "it compiles" means "it ships." Compiling proves the source is internally consistent. Packaging proves the artifact is externally consistent. They are different consistencies and they fail differently.

The publish itself was anticlimactic. Five vsce publish commands. One timeout retry. Five extensions live. Total elapsed: about three minutes after I had the artifacts right.

The hard part was always making the artifacts right.

Read this post in the full Engineering Journal →