Skip to content

Expose broad public API surface#760

Merged
malberts merged 1 commit intofrontend-extensibilityfrom
broad-public-api
Apr 23, 2026
Merged

Expose broad public API surface#760
malberts merged 1 commit intofrontend-extensibilityfrom
broad-public-api

Conversation

@malberts
Copy link
Copy Markdown
Collaborator

@malberts malberts commented Apr 22, 2026

Stacks on top of #754.

Responds to Jeroen's narrow-vs-broad review point: the alpha surface on ext.neowiki is broadened so downstream consumers don't hit "not exported" friction during build-out. Curation (narrowing) is deferred to a pre-production gate.

Change

Expands resources/ext.neowiki/src/public-api.ts from the narrow 4-symbol barrel (what RedHerb's DateTime happens to need today) to a wildcard re-export of every TS module and every Vue component under resources/ext.neowiki/src/. Extensions via require('ext.neowiki') can now reach:

  • Every domain type, value object, and registry
  • Every application-layer lookup, repository, and authorizer
  • Every persistence serializer, deserializer, and REST adapter
  • Every Pinia store
  • Every composable
  • Every Vue component (the full component library)
  • Every TypeScript type

Runtime-verified: 146 named exports on require('ext.neowiki'). Spot-checked in the browser — newStringValue, SubjectValidator, RestSchemaRepository, useSubjectStore, Infobox, SchemaEditorDialog, ValueType, PropertyName, FrontendRegistrar all resolve.

Implementation choices considered

A. Hand-curated (this PR)

public-api.ts lists every module with export * from './path' for TS files and export { default as Name } from './path.vue' for Vue files. ~130 lines today, one line per source file.

export * from './Neo';
export * from './NeoWikiServices';
export * from './domain/Value';
// … ~80 more TS lines …
export { default as Infobox } from './components/Views/Infobox.vue';
export { default as SchemaEditorDialog } from './components/SchemaEditor/SchemaEditorDialog.vue';
// … ~45 more Vue lines …

Pros: explicit; reviewer sees every public symbol in the diff; works in any bundler; compile error at the line when a collision is introduced; no build tooling.
Cons: one line of maintenance per new file — but see Maintenance discussion below.

B. Pre-build codegen script

A small Node/TS script globs src/, applies exclude rules (tests, *.d.ts, etc.), and emits public-api.ts. Script is invoked either as an npm prebuild hook or a Vite plugin. The generated file is either checked in (noisy rename diffs, but reviewer-visible) or gitignored and regenerated on install/build (cleaner diffs, opaque to reviewers).

Example script shape (pseudocode):

// scripts/generate-public-api.mjs
import { writeFileSync } from 'fs';
import { globSync } from 'glob';

const exclude = new Set( [ 'neowiki.ts', 'public-api.ts', 'mediawiki-vue.d.ts', 'TestHelpers.ts' ] );
const lines = [ "import './neowiki';" ];

for ( const file of globSync( 'src/**/*.ts' ).sort() ) {
    if ( exclude.has( path.basename( file ) ) || file.includes( '__tests__' ) ) continue;
    lines.push( `export * from './${ file.replace( /^src\//, '' ).replace( /\.ts$/, '' ) }';` );
}
for ( const file of globSync( 'src/**/*.vue' ).sort() ) {
    const name = path.basename( file, '.vue' );
    lines.push( `export { default as ${ name } } from './${ file.replace( /^src\//, '' ) }';` );
}

writeFileSync( 'src/public-api.ts', lines.join( '\n' ) + '\n' );

Then "prebuild": "node scripts/generate-public-api.mjs" in package.json.

Pros: zero per-file maintenance; guaranteed no-un-exported-files.
Cons: adds a script + build step; generated-and-gitignored hides the surface from reviewers (they have to run the build to see what's public); generated-and-checked-in is noisier than hand-curated on every file rename; debugging the generated file is awkward (not a place humans edit).

C. Vite import.meta.glob

Single-line exposure via Vite-specific syntax:

import './neowiki';
export const modules = import.meta.glob( './**/*.{ts,vue}', { eager: true } );

Consumers access by file path:

require( 'ext.neowiki' ).modules[ './domain/Value.ts' ].newStringValue( 'x' );
require( 'ext.neowiki' ).modules[ './components/Views/Infobox.vue' ].default;

To flatten into a single namespace (closer to today's ergonomics), we'd post-process:

const modules = import.meta.glob( './**/*.ts', { eager: true } );
const flat: Record<string, unknown> = {};
for ( const mod of Object.values( modules ) ) {
    Object.assign( flat, mod ); // last-write-wins on collisions — silent!
}
export default flat;

Pros: truly zero maintenance; accurate by construction.
Cons: Vite-specific (won't work with other bundlers if we ever swap); file-path-keyed access is awkward for consumers; flattening silently overwrites on name collisions (hand-curated would error at build); TypeScript can't see through the glob so type exports are lost — consumers get runtime values but no type info.

Maintenance: why A wins for us

Two points make the hand-curated / approach-A choice clearer than the pros/cons above suggest:

1. AI-assisted development kills the "one line per file" cost.
With Claude Code (or similar) in the dev loop:

  • Adding a file: the AI adds the barrel line in the same edit. Same PR, same diff.
  • Renaming / moving: the AI updates the barrel path alongside the other required import-site updates.
  • Removing: the AI drops the barrel line alongside the deletion.
  • Forgetting: trivially caught in review ("you added Foo.ts but no barrel line").

The historical human-cognitive-load case against hand-curated no longer applies.

2. We plan to narrow the surface before production anyway.
Autogen's (approaches B/C) value is "automatically expose everything, forever". The moment we want a curated subset, autogen becomes impossible by definition — curation is inherently manual. Investing in autogen during alpha would just be temporary infrastructure with a guaranteed end-of-life when we switch to the stable contract. Sticking with hand-curated now means zero transition cost later: we simply delete the lines we don't want to expose.

So: autogen only makes sense if broad-forever is the plan. It isn't. Hand-curated is correct for this project's full trajectory.

Stability contract

The barrel comment explicitly calls out 0.x / alpha — anything can change without notice. Consumers should pin specific NeoWiki commits during the alpha phase. A curation pass to narrow the surface is planned before production stabilisation.

Bundle impact

  • Raw: 240 KB → 251 KB (+11 KB / +4%)
  • Gzipped: 63 KB → 66 KB (+3 KB / +5%)

Modest. Acceptable for the flexibility it provides during alpha.

Test plan

  • make ts-build green — ~260 modules transformed, no name collisions (wildcard export * errors out on conflicts; there are none today)
  • make ts-test — 684 tests pass
  • make ts-lint — clean
  • Bundle inspection: 146 named runtime exports (was 4 in the narrow baseline)
  • Runtime probe in the browser: 9/9 spot-checks pass across value factories, domain classes, persistence adapters, stores, components, enums, and presentation classes
  • Manual: in the browser with the existing RedHerb setup, confirm nothing regresses (same flows as Add JavaScript frontend extensibility #754)

What this PR does not validate

The runtime probe confirms that 146 names are now accessible via require('ext.neowiki'). That's a bindings check, not a behavioural one. None of the newly-exposed symbols — stores, composables, domain services, repositories, or any of the Vue components beyond NeoNestedField — have been exercised from an actual extension-consumer context.

Concretely, we have not verified:

  • Stores work correctly when consumed from an extension's Vue app. Pinia instance sharing across mounts is a known potential footgun (flagged in Jeroen's review); unexercised here.
  • Composables (e.g. useChangeDetection, useCloseConfirmation) function outside the components they were designed alongside.
  • Repositories (RestSchemaRepository, RestLayoutRepository, etc.) don't depend on NeoWiki-internal injected context that's absent when instantiated from extension code.
  • Vue components render correctly when mounted under an extension's Vue app with different providers / pinia setup.
  • Serializers/deserializers round-trip as expected when called from outside the NeoWiki app context.

RedHerb's DateTime today exercises exactly four symbols (newStringValue, ValueType, NeoWikiServices, NeoNestedField) — the rest of the 146 are available but untested as a public contract. Expect bugs when consumers start actually using them; the alpha stability caveat covers this, but the gap is worth naming explicitly.

Follow-ups

  • Curation pass before production — audit which symbols are useful to consumers vs. internal-only; narrow the barrel and document the stable contract. Track as its own issue.
  • Name-collision regression guard — if two modules ever grow an identically-named export, export * will error at build. Fine for now; worth a script/check if the source tree grows a lot.

@malberts malberts force-pushed the frontend-extensibility branch from 6da22a2 to bd8f5d3 Compare April 22, 2026 22:57
@malberts malberts marked this pull request as ready for review April 22, 2026 23:12
@malberts malberts requested a review from JeroenDeDauw April 22, 2026 23:13
@malberts malberts force-pushed the frontend-extensibility branch from bd8f5d3 to bb0782c Compare April 22, 2026 23:36
Re-export every TS module (via wildcard) and every Vue component under
resources/ext.neowiki/src/ from the public-api barrel. Extensions using
require('ext.neowiki') can now reach anything NeoWiki itself exposes -
domain types and services, application-layer lookups and repositories,
persistence serializers/deserializers, stores, composables, and the
full component library.

0.x / alpha stability contract: the surface is provisional and any
symbol may be renamed or removed without migration guidance. A
curation pass to narrow the public API is planned before production
stabilisation. Consumers should pin specific NeoWiki commits during
the alpha phase.

Bundle size impact: 240 KB -> 251 KB raw (+11 KB / +4%), 63 KB -> 66 KB
gzipped (+3 KB / +5%). Acceptable for the flexibility it provides
during alpha build-out.

Stacks on top of #754.
@malberts malberts merged commit dc3dce6 into frontend-extensibility Apr 23, 2026
9 checks passed
@malberts malberts deleted the broad-public-api branch April 23, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant