RSPFx Internal API Contract
Single source of truth for package public surfaces. Subagents implement EXACTLY these signatures — no invented names, no scope creep. Read reference/FORMATS.md for SharePoint formats. ESM only ("type": "module"). Strict TS. No comments unless requested. Zero webpack/heft/gulp dependencies anywhere.
Shared conventions
- Packages:
@mbsks/rspfx-<name>, version0.0.14, ESM,main/types→dist/; all 20 publishable packages (packages/*+apps/cli) share one version bumped together viascripts/publish.mjs:17;examples/*andapps/playgroundareprivate:trueand excluded. - Native acceleration (optional):
crates/rspfx-sppkg,crates/rspfx-manifest,crates/rspfx-rspack-pluginprovide Rust implementations with JS fallback (try { require('../../crates/.../index.node') } catch {}); no.noderequired —bun run buildandbun run testpass with or without native. - All packages build with
tsctodist/; typecheck:bunx --cwd tsc --noEmit -p <pkg>/tsconfig.json. - Errors: throw
RspfxError(code, message, cause?)from@mbsks/rspfx-diagnostics. - Tests: vitest, colocated
tests/*.test.ts, happy-dom only where DOM needed.
@mbsks/rspfx-core (zero dependencies)
packages/core/src/index.ts exports:
export type FrameworkId = 'vanilla' | 'react' | 'solid' | 'vue' | 'preact' | 'svelte' | (string & {});
export type SpfxTarget = '1.20' | '1.21' | '1.22' | '1.23' | '1.24';
export interface DevConfig {
port?: number; // default 4321 (manifest+bundle server, like official serve)
https?: boolean; // default true
hostname?: string; // default 'localhost'
workbench?: boolean; // default true — auto-open workbench page
fastRefresh?: boolean; // default false (rspfx dev --refresh)
openBrowser?: boolean; // default false — opt in with rspfx dev --browser
tenantUrl?: string; // e.g. https://contoso.sharepoint.com
initialPage?: string; // overrides tenantUrl; supports {tenantdomain} token
}
export interface BuildConfig {
sourcemap?: boolean; // @deprecated — set devtool in bundler config; scaffold-only default false
minify?: boolean; // @deprecated — set optimization.minimize / build.minify in bundler config; scaffold-only default true
splitChunks?: boolean; // @deprecated — must remain false for SPFx AMD correctness; scaffold-only default false
outDir?: string; // default 'dist'
releaseDir?: string; // default 'release'
}
export interface DeployConfig {
tenantUrl?: string; username?: string; password?: string;
appCatalogSiteUrl?: string; // e.g. https://contoso.sharepoint.com/sites/appcatalog
}
export type FrameworkIdCore = 'vanilla' | 'react' | 'solid' | 'vue' | 'preact' | 'svelte';
export type FrameworkId = FrameworkIdCore | (string & { __custom?: never });
export interface RspfxConfig {
name: string; // project name (npm name)
version?: string; // build-time version for AMD library names + manifests; overrides package.json
framework: FrameworkId;
spfxVersion: SpfxTarget; // default '1.23'
dev: DevConfig;
build: BuildConfig;
paths?: PathsConfig;
deploy?: DeployConfig;
teams?: boolean | TeamsConfig;
}
export const RspfxConfigSchema: unknown; // strictObject — rejects unknown keys
export type Issue = { path: (string|number)[]; message: string; code: string };
export function defineConfig<const T extends RspfxConfig>(config: T): T;
export function parseRSPFXConfig(raw: unknown): Result<RspfxConfig, Issue[]>;
export function tryResolveConfig(raw: unknown): Result<RspfxConfig, Issue[]>;
export function resolveConfig(config: Partial<RspfxConfig>): RspfxConfig; // fills defaults — throws RspfxError(CONFIG_VALIDATION_FAILED) on Issue[]
export const RSPFX_PLUGIN_MARKER: symbol; // Symbol.for('@mbsks/rspfx/bundler-plugin'); stamped on bundler plugin instances
export interface RspfxBundlerPluginLike { // structural contract for RspfxPlugin / rspfxVite
options: RspfxConfig;
[key: symbol]: unknown;
}
// SPFx-mirror types (structural, no @microsoft dependency)
export enum EnvironmentType { Local = 0, ClassicSharePoint = 1, SharePoint = 2 }
export enum PropertyPaneFieldType {
Custom = 1, CheckBox = 2, TextField = 3, Dropdown = 4, Toggle = 5, Link = 6,
Slider = 7, Heading = 8, ChoiceGroup = 9, Button = 10, HorizontalRule = 11,
Image = 12, Thumbnail = 13, ColorPicker = 14, SpinButton = 15, Label = 16,
DynamicField = 17, DynamicFieldSet = 18, DynamicData = 19,
}
export class Version { // mirrors sp-core-library Version
constructor(major: number, minor: number, patch: number, build?: number);
static parse(versionString: string): Version;
static tryParse(versionString: string): Version | undefined;
static compare(v1: string, v2: string): number; // -1 | 0 | 1
readonly major: number; readonly minor: number; readonly patch: number; readonly build: number;
toString(): string; compareTo(other: Version): number;
}
export interface ISpfxTheme { palette: Record<string, string>; [k: string]: unknown }
export interface ThemeProvider {
getTheme(): ISpfxTheme | undefined;
addChangeListener(listener: () => void): void;
removeChangeListener(listener: (() => void)): void;
}
export interface WebPartContextLike { // minimal surface used by web parts/templates
instanceId: string;
webPartTag: string;
domElement: HTMLElement;
properties: Record<string, unknown>;
environment: { type: EnvironmentType };
pageContext: { web: { title: string; absoluteUrl: string }; site: { absoluteUrl: string } };
themeProvider?: ThemeProvider;
propertyPane: Record<string, unknown>;
[key: string]: unknown;
}
export interface HeadlessAdapter<TProps extends Record<string, unknown>> {
readonly mount: (root: HTMLElement, props: TProps) => void;
readonly update: (root: HTMLElement, props: TProps) => void;
readonly unmount: (root: HTMLElement) => void;
}
export interface HeadlessContext {
readonly domElement: HTMLElement;
readonly theme: ISpfxTheme | undefined;
readonly themeProvider?: ThemeProvider;
readonly environment: EnvironmentType;
readonly cultureName: string;
readonly manifestId?: string;
}
export type PropsSelector<TProps, TRaw = Record<string, unknown>> = (raw: TRaw, ctx: HeadlessContext) => TProps;HeadlessAdapter lives in src/headless.ts, exported via @mbsks/rspfx-core/headless (zero-deps).
@mbsks/rspfx-webpart-base (depends on core + @microsoft/sp-webpart-base)
export abstract class HeadlessWebPart<TProps extends Record<string, unknown> = Record<string, unknown>> extends BaseClientSideWebPart<TProps> {
protected abstract createAdapter(): HeadlessAdapter<TProps>;
protected getComponentProps(): TProps; // default: this.properties as TProps
public override render(): void; // adapter.mount(this.domElement, getComponentProps())
protected override onDispose(): void; // adapter.unmount + super
protected updateProps(next: TProps): void; // adapter.update
}
export function defineWebPart<const TProps extends Record<string, unknown>>(opts: {
readonly adapterFactory: (host: { domElement: HTMLElement }) => HeadlessAdapter<TProps>;
readonly selector?: PropsSelector<TProps>;
readonly propertiesSchema?: (raw: unknown) => TProps;
readonly displayName?: string;
readonly getPropertyPaneConfiguration?: () => unknown;
}): new () => HeadlessWebPart<TProps>;
/** @deprecated alias for HeadlessWebPart — use @mbsks/rspfx-webpart-base */
export const BaseWebPart = HeadlessWebPart;Compat shim at @mbsks/rspfx-core/webpart re-exports HeadlessWebPart as BaseWebPart with a one-time console.warn('deprecated: use @mbsks/rspfx-webpart-base').
@mbsks/rspfx-plugin-api (depends on core, diagnostics)
export interface FrameworkRspackContributions { // structural; typed loosely to stay compiler-agnostic
rules?: unknown[]; // rspack RuleSetRule[]
plugins?: unknown[]; // rspack plugin instances
resolve?: { alias?: Record<string, string>; extensions?: string[] };
swc?: { jsc?: { parser?: unknown; transform?: unknown } }; // for builtin:swc-loader options
define?: Record<string, string>;
moduleTest?: { test?: RegExp; type?: string }; // e.g. { type: 'asset' } for css
}
export interface FrameworkViteContributions { // vite-shaped; merged into the vite config by rspfxVite
plugins?: unknown[]; // vite plugin instances (@vitejs/plugin-react, @vitejs/plugin-vue, @prefresh/vite, @sveltejs/vite-plugin-svelte, vite-plugin-solid)
esbuild?: Record<string, unknown>; // esbuild transform options, e.g. { jsx: 'automatic' }
resolveExtensions?: string[];
define?: Record<string, string>;
}
export interface FrameworkRsbuildContributions { // webpack/rspack-shaped, minus the swc block (Rsbuild owns SWC)
rules?: unknown[]; // loader strings rewritten to absolute paths via resolveContributionLoaders
plugins?: unknown[]; // rspack plugin instances
resolve?: { alias?: Record<string, string>; extensions?: string[] };
define?: Record<string, string>;
}
export interface FrameworkPreset<F extends string = import('@mbsks/rspfx-core').FrameworkId> {
name: F; // custom frameworks use FrameworkPreset<string> — FrameworkId is 'vanilla' | 'react' | 'solid' | 'vue' | 'preact' | 'svelte' | (string & {})
contributions(opts: { fastRefresh: boolean }): FrameworkRspackContributions;
vite?(opts: { fastRefresh: boolean }): FrameworkViteContributions; // optional — absent = no Vite support; rspfxVite warns loudly
rsbuild?(opts: { fastRefresh: boolean }): FrameworkRsbuildContributions; // optional — rspfxRsbuild falls back to contributions() when absent
}
export const HOOK_PHASES = ['beforeCompile','afterCompile','afterStats','beforeGenerate','afterGenerate','beforeStart','afterStart','beforePackage','afterPackage'] as const;
export type HookPhase = typeof HOOK_PHASES[number];
export type HookResult<T> = Result<T, RspfxError | AggregateRspfxError>;
export type BeforeCompile = (ctx: CompileContext) => HookResult<CompileContext> | void | Promise<HookResult<CompileContext>|void>;
export type AfterStats = (stats: Stats) => void | Promise<void>;
export type BeforeGenerate = (ctx: { readonly production: boolean; readonly webParts: readonly WebPartEntry[] }) => HookResult<typeof ctx> | void | Promise<HookResult<typeof ctx>|void>;
export type AfterGenerate = (ctx: { readonly manifests: readonly ComponentManifest[]; readonly releaseDir: string }) => void | Promise<void>;
export type BeforePackage = (ctx: { readonly manifests: readonly ComponentManifest[]; readonly files: ReadonlyMap<ZipPath, Uint8Array> }) => HookResult<ReadonlyMap<ZipPath,Uint8Array>> | ReadonlyMap<ZipPath,Uint8Array> | void | Promise<...>;
export type AfterPackage = (ctx: { readonly sppkgPath: ZipPath }) => void | Promise<void>;
export type OnHookError = (err: RspfxError, phase: HookPhase, pluginName: string) => 'throw' | 'continue';
export interface CompilerHooks { beforeCompile?: BeforeCompile; afterStats?: AfterStats; afterCompile?: AfterStats; }
export interface ReleaseHooks { beforeGenerate?: BeforeGenerate; afterGenerate?: AfterGenerate; }
export interface DevHooks { beforeStart?: (ctx: { readonly mode: 'local'|'sharepoint'; readonly port?: number }) => HookResult<typeof ctx>|void | Promise<...> ; afterStart?: (ctx: { readonly url: string }) => void | Promise<void>; }
export interface PackageHooks { beforePackage?: BeforePackage; afterPackage?: AfterPackage; }
export interface SpfxVersionPatch { readonly target: string; readonly npmVersion: string; readonly toolchain: 'gulp' | 'heft'; readonly status: 'ga' | 'preview'; readonly notes?: string; }
export type ComponentIdsPatch = Record<string, { id: string; version: string; preloadComponents?: string[] }>;
export interface RspfxPatches {
readonly findSpDependencies?: (args: { projectRoot: string } | string, next: (args: { projectRoot: string } | string) => Map<string, { id: string; version: string; manifestPath: string }>) => Map<string, { id: string; version: string; manifestPath: string }> | Promise<Map<string, { id: string; version: string; manifestPath: string }>>;
readonly generateComponentManifests?: (args: { projectRoot: string; production: boolean; baseUrls: { debug: string; release: string[] }; packageVersion: string; bundleFiles: Map<string, string>; externals: string[]; webpartsDir?: string; entryModuleIds?: Record<string, string> }, next: (args: typeof args) => Promise<ComponentManifest[]>) => Promise<ComponentManifest[]>;
readonly buildAppManifestXml?: (args: { name: string; productId: string; version?: string; skipFeatureDeployment: boolean; isDomainIsolated?: boolean; spfxVersion?: string; developer?: Record<string, unknown>; metadata?: Record<string, unknown>; localizedStrings?: { locale: string; values: Record<string, string> }[]; webApiPermissionRequests?: { resource: string; scope: string }[]; pretty: boolean }, next: (args: typeof args) => string) => string | Promise<string>;
readonly generateManifestsJs?: (manifests: ComponentManifest[], metadata: unknown, next: (manifests: ComponentManifest[], metadata: unknown) => Promise<string>) => Promise<string>;
readonly buildPackage?: (opts: unknown, next: (opts: unknown) => Promise<{ outputPath: string; zipEntries: string[]; appManifest: string }>) => Promise<{ outputPath: string; zipEntries: string[]; appManifest: string }>;
}
export interface RspfxExtension {
readonly name: string;
readonly frameworkPreset?: FrameworkPreset;
readonly compilerHooks?: CompilerHooks;
readonly releaseHooks?: ReleaseHooks;
readonly devHooks?: DevHooks;
readonly packageHooks?: PackageHooks;
readonly onError?: OnHookError;
readonly priority?: number;
readonly spfxVersions?: readonly (SpfxVersionPatch | import('@mbsks/rspfx-core').SpfxVersionInfo)[];
readonly spfxVersion?: SpfxVersionPatch | import('@mbsks/rspfx-core').SpfxVersionInfo;
readonly componentIds?: ComponentIdsPatch;
readonly patches?: RspfxPatches;
}
export function definePlugin(plugin: RspfxExtension): RspfxExtension;
export function registerPlugin(plugin: RspfxExtension): void; // global registry; read by the CLI before each build/package
export function getPlugins(): RspfxExtension[];
export function getPatchedSpfxVersions(base: readonly SpfxVersionPatch[], plugins: readonly RspfxExtension[]): readonly SpfxVersionPatch[]; // merges base SPFX_VERSIONS with extension spfxVersions in packages/plugin-api/src/patches.ts:3
export function getPatchedComponentIds(base: Record<string, { id: string; version: string }>, plugins: readonly RspfxExtension[]): Record<string, { id: string; version: string }>; // merges SP_COMPONENT_IDS with extension componentIds in packages/plugin-api/src/patches.ts:12
export interface HookBus {
readonly plugins: readonly RspfxExtension[];
emitBeforeCompile(ctx: CompileContext): Promise<HookResult<CompileContext>>;
emitAfterStats(stats: Stats): Promise<void>;
emitBeforeGenerate(ctx: { readonly production: boolean; readonly webParts: readonly WebPartEntry[] }): Promise<HookResult<typeof ctx>>;
emitAfterGenerate(ctx: { readonly manifests: readonly ComponentManifest[]; readonly releaseDir: string }): Promise<void>;
emitBeforeStart(ctx: { readonly mode: 'local'|'sharepoint'; readonly port?: number }): Promise<HookResult<typeof ctx>>;
emitAfterStart(ctx: { readonly url: string }): Promise<void>;
emitBeforePackage(ctx: { readonly manifests: readonly ComponentManifest[]; readonly files: ReadonlyMap<ZipPath,Uint8Array> }): Promise<HookResult<ReadonlyMap<ZipPath,Uint8Array>>>;
emitAfterPackage(ctx: { readonly sppkgPath: ZipPath }): Promise<void>;
hasPatch(name: keyof RspfxPatches): boolean;
getPatch<K extends keyof RspfxPatches>(name: K): RspfxPatches[K] | undefined;
callWithPatch<T>(name: keyof RspfxPatches, args: unknown, next: (args: unknown) => T | Promise<T>): Promise<T>;
getMergedSpfxVersions(): readonly (SpfxVersionPatch | import('@mbsks/rspfx-core').SpfxVersionInfo)[];
getMergedComponentIds(): ComponentIdsPatch;
}
export function createHookBus(plugins: readonly RspfxExtension[], opts?: { logger?: Logger; onError?: OnHookError }): HookBus;
export function composeHooks<T>(...hooks: Array<(ctx: T) => HookResult<T>|void>): (ctx: T) => Promise<HookResult<T>>;
export function sortedPlugins(plugins: readonly RspfxExtension[]): readonly RspfxExtension[];The hooks are wired at fixed points in the shared pipeline; with no registered plugins the loops are empty no-ops and behavior is unchanged:
vite()/rsbuild()are the M8 bundler-parity surface.rspfxVitecallspreset.vite({ fastRefresh })and merges plugins/esbuild/resolveExtensions/define into the vite config (loud warning when the method is absent — no Vite support for that framework).rspfxRsbuildcallspreset.rsbuild({ fastRefresh })inmodifyRspackConfigand merges rules/plugins/resolve/define (loader strings rewritten against the framework package's ownnode_modulesbyresolveContributionLoaders); whenrsbuild()is absent it falls back tocontributions()minus the swc block. React/preact rsbuild refresh is babel-based (react-refresh/babel,@prefresh/babel-plugin); vue/svelte/solid rsbuild reuse their rspack loader rules.compilerHooks.beforeCompile— invoked with the resolvedCompileContextbeforebuild()runs; plugins mutate it in place (e.g. push intoadditionalPluginsorswcContributions). The return value is ignored. (CLI rspack path only.)compilerHooks.afterStats— invoked with the rspack stats oncebuild()resolves.releaseHooks.beforeGenerate/releaseHooks.afterGenerate— fired byassembleRelease(packages/dev-runtime/src/release.ts) around component-manifest generation, so they run identically for every entry point:rspfx build(CLI) and the native bundler commands (vite build,rspack build,rsbuild build).devHooks.beforeStart/devHooks.afterStart— fired around dev-server startup byrspfx dev(rspack path) and by the vite/rsbuild plugins (configureServer,onBeforeStartDevServer/onAfterStartDevServer).packageHooks.beforePackage— invoked with{ manifests, files }before the.sppkgis assembled (rspfx package);manifestsare the release component manifests,filesare the release assets as{ path, content }so plugins can add or transform package files.packageHooks.afterPackage— invoked with{ sppkgPath }once the.sppkgis written.
@mbsks/rspfx-diagnostics (depends on core only)
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'success' | 'trace';
export type LogFields = Record<string, string|number|boolean|undefined>;
export interface LogEntry { readonly level: LogLevel; readonly name: string; readonly message: string; readonly fields: Readonly<LogFields>; readonly timestamp: string; readonly error?: unknown; }
export interface LoggerOptions { readonly level?: LogLevel; readonly json?: boolean; readonly sinks?: Array<(e: LogEntry)=>void>; }
export interface Logger {
readonly name: string;
error(message: string, fields?: LogFields): void;
warn(message: string, fields?: LogFields): void;
info(message: string, fields?: LogFields): void;
debug(message: string, fields?: LogFields): void;
success(message: string, fields?: LogFields): void;
trace(message: string, fields?: LogFields): void;
child(fields: LogFields): Logger;
isLevelEnabled(level: LogLevel): boolean;
withLevel(level: LogLevel): Logger;
}
export function createLogger(name: string, opts?: LoggerOptions): Logger;
export function createDiagnosticFormatter(logger: Logger): (err: RspfxError|AggregateRspfxError)=>string;
export class RspfxError extends Error { readonly code: RspfxErrorCode; readonly cause?: RspfxError|Error; constructor(code: RspfxErrorCode, message: string, cause?: RspfxError|Error) }
export class AggregateRspfxError extends Error { readonly code: RspfxErrorCode.AGGREGATE; readonly errors: readonly RspfxError[]; constructor(errors: readonly RspfxError[], message?: string) }
export function isRspfxError(e: unknown): e is RspfxError | AggregateRspfxError;
export function isAggregateRspfxError(e: unknown): e is AggregateRspfxError;
export function flatCauseChain(err: RspfxError): RspfxError[];
export function formatError(err: RspfxError|AggregateRspfxError, opts?: { color?: boolean }): string;
export async function trace<T>(name: string, fn: () => Promise<T>): Promise<T>; // logs duration
export function timeStart(name: string): () => number; // returns elapsed ms fn
export function reportBenchmark(name: string, ms: number): void; // appends .rspfx/benchmarks.jsonl
export function formatBytes(bytes: number): string;
export interface Tracer { span<T>(name: string, fn: ()=>Promise<T>): Promise<T>; time<T>(name: string, fn: ()=>T): T; }
export function createTracer(logger: Logger): Tracer;@mbsks/rspfx-compiler-rspack (depends on core, plugin-api, diagnostics)
export interface BundleEntry {
name: string; // bundleName — manifests' loaderConfig.entryModuleId follows the bundle name (config.json bundle key, else webpart folder name)
import: string; // absolute path to entry .ts/.tsx
componentIds: string[]; // manifest ids in this bundle (for library name)
version: string; // package version (library name: `${id}_${version}`)
}
export interface CompileContext {
projectRoot: string;
framework: FrameworkId;
fastRefresh: boolean;
production: boolean;
entries: BundleEntry[];
externals: string[]; // package names (sp-* etc.)
build: BuildConfig; // from core
serveMode?: boolean; // dev server mode (no contenthash)
additionalPlugins?: unknown[]; // from plugin registry
swcContributions?: Record<string, unknown>[]; // from framework presets
}
export async function createRspackConfig(ctx: CompileContext): Promise<unknown>;
export async function build(ctx: CompileContext): Promise<{ stats: unknown; outputFiles: string[] }>; // writes dist/
export function watch(ctx: CompileContext, onDone: (stats: unknown, errors: unknown[]) => void): { close(): Promise<void> };
// dev server (rspack + @rspack/dev-server) — called by dev-runtime; returns started server
export async function startDevServer(ctx: CompileContext, devServerOptions: unknown): Promise<{ close(): Promise<void>; port: number; compiler: unknown; onEmit(cb: () => void): void }>;@mbsks/rspfx-plugin (depends on core, compiler-rspack, dev-runtime, manifest-generator, manifest-server, diagnostics, plugin-api, @rspack/core)
The project config as bundler plugins — the replacement for the legacy rspfx.config.ts. The CLI loads the user's rspack.config.ts / vite.config.ts / rsbuild.config.ts via jiti, scans the plugins array for the marker symbol, and reads the resolved options (project config). No plugin → CLI error with guidance.
export type { RspfxConfig } from '@mbsks/rspfx-core';
export { defineConfig, resolveConfig, RSPFX_PLUGIN_MARKER } from '@mbsks/rspfx-core';
// RSPFX_PLUGIN_MARKER: Symbol.for('@mbsks/rspfx/bundler-plugin') — stable across
// duplicated package copies; stamped on every plugin instance
export interface RspfxPluginOptions extends Partial<Omit<RspfxConfig, 'name'>> {
name: string;
projectRoot?: string; // defaults to process.cwd()
}
// options carry: name, version (build-time version used in AMD library names and
// manifests — overrides package.json), spfxVersion, framework,
// dev (port/https/hostname/workbench/fastRefresh/openBrowser/tenantUrl/initialPage),
// build (sourcemap/minify/splitChunks/outDir/releaseDir), paths (srcDir/webpartsDir/configDir),
// deploy; defaults unchanged.
export class RspfxPlugin implements RspfxBundlerPluginLike {
readonly [RSPFX_PLUGIN_MARKER]: true;
readonly options: RspfxConfig;
constructor(options: RspfxPluginOptions);
apply(compiler: unknown): void; // standard webpack plugin interface — webpack-compatible bundlers (Rspack) can run the compile-time parts; Turbopack does not support webpack plugins
}
export function rspfxVite(options: RspfxPluginOptions): ViteRspfxPlugin; // { name: 'rspfx', [RSPFX_PLUGIN_MARKER]: true, options }
export const VITE_ENV: { mode: string; entry: string; amdId: string; fastRefresh: string };
// env var names for per-bundle vite builds: RSPFX_VITE_MODE / RSPFX_VITE_ENTRY /
// RSPFX_VITE_AMD_ID, plus RSPFX_FAST_REFRESH ('1' gates fast refresh in dev;
// also enabled by dev.fastRefresh)
export function rspfxRsbuild(options: RspfxPluginOptions): RsbuildRspfxPlugin;
// { name: 'rspfx-rsbuild', [RSPFX_PLUGIN_MARKER]: true, options, setup(api) } — RsbuildPlugin-compatible
// setup: modifyRsbuildConfig (html: false, distPath.root = build.outDir, source.entry) +
// modifyRspackConfig (AMD library entries, externals, [name].js output, chunkLoadingGlobal
// webpackJsonp_<uniqueName>, SPFX_PUBLIC_PATH_SENTINEL publicPath, localized aliases, and the
// SpfxPublicPathPlugin / SpfxLocalizedResourcesPlugin / DefinePlugin instances)@mbsks/rspfx-core exports the structural marker contract: RSPFX_PLUGIN_MARKER and RspfxBundlerPluginLike ({ options: RspfxConfig }).
The full pipeline (manifests, dev server, packaging) runs through the rspfx CLI; for Vite configs rspfx build/rspfx package spawn one vite build per web part bundle (selected via VITE_ENV env vars), and rspfx dev spawns vite (the plugin serves /temp/manifests.js, rebuilds AMD bundles into dist/, opens the workbench when a tenant is configured). For Rsbuild configs a single rsbuild build produces all web part bundles and rspfx dev spawns rsbuild dev. The local preview page and mock /_api API are served by dev-runtime's startServe, which the CLI runs on the Rspack path — the Vite/Rsbuild dev flows are workbench-only for now.
M8 parity surface (shared with the Rspack path):
rspfxViteloads the framework preset and merges itsvite()contributions (loud warning when the framework has none). Each bundle is closed to Rspack byte-compat output: thescriptUrlCaptureLineis prepended (same bytes as the RspackSpfxPublicPathPlugin), theSPFX_PUBLIC_PATH_SENTINELis rewritten to the script-URL public-path expression, and emitted.cssassets are inlined into the JS bundle (cssCodeSplit: false; no.cssfiles indist/).- Both plugins write
.rspfx/stats.jsonas{ "moduleCounts": { "<bundle>": n } }(per entry for vite; per entry chunk for rsbuild viaonAfterBuild) — therspfx analyzemodule counts for bundlers that emit no webpack-style stats. - Fast refresh is gated on dev mode + (
RSPFX_FAST_REFRESH=1ordev.fastRefresh): vite merges the preset's refresh plugins (@vitejs/plugin-react,@prefresh/vite, …); rsbuild merges the preset'srsbuild()rules/plugins inmodifyRspackConfig(no swc block — Rsbuild owns SWC; loader strings rewritten viaresolveContributionLoaders). - Dev auto-reload: after each rebuild the reload controller is ticked and bundle URLs in
/temp/manifests.jsget a?t=<epoch>cache-busting suffix (both vite and rsbuild paths).
Notes for implementer:
- Use
builtin:swc-loaderfor .ts/.tsx/.jsx/.js (parser jsx, decorators, importMeta); merge framework swc contributions. - SCSS:
sass-loader+sass; CSS modules viaexperiments.css+.module.*convention; no Tailwind special-casing — users wire their own CSS tooling (Tailwind, UnoCSS, …) in the bundler config. - Output: filename
[name].js(always — manifest references exact name), chunkFilenamechunk.[name].js,output.library: { type: 'amd', name: '<componentId>_<version>' }(single component per bundle — M1 default),externals: string[],chunkLoadingGlobal: webpackJsonp_<uniqueName>where uniqueName = single id_version or md5 hex of concatenated ids,crossOriginLoading: 'anonymous',publicPath: 'auto',devtool: production ? (sourcemap?'hidden-source-map':false) : 'source-map'(rspack value 'hidden-source-map' supported). - DefinePlugin: DEBUG, DEPRECATED_UNIT_TEST, process.env.NODE_ENV.
- CSS inlined into JS bundle (SPFx has no external css in sppkg). Implement a tiny inline-css loader (or style-loader equivalent) — do NOT use css-extract.
- optimization: moduleIds 'named' in dev / 'deterministic' in production, usedExports, sideEffects, removeEmptyChunks;
minimizeonly in production (mode === 'production' && build.minify). experiments.css: trueonly if stable; otherwise css-loader+style-loader chain. Verify in tests.- Caching: rspack
cache: { type: 'filesystem' }in watch mode.
@mbsks/rspfx-manifest-generator (depends on core, diagnostics)
export interface ComponentManifest { id: string; alias: string; componentType: string; version: string; manifestVersion: number; loaderConfig: { internalModuleBaseUrls: string[]; entryModuleId: string; scriptResources: Record<string, unknown>; exportName?: string }; [k: string]: unknown }
export interface ManifestContext {
projectRoot: string;
production: boolean;
baseUrls: { debug: string; release: string[] }; // debug: 'https://localhost:4321/dist/'; release: cdn or []
packageVersion: string; // package.json version
bundleFiles: Map<string, string>; // bundleName -> emitted js filename
externals: string[]; // externalized names → "type": "component" scriptResources
webpartsDir?: string; // overrides default 'src/webparts' (config.paths.webpartsDir)
entryModuleIds?: Record<string, string>; // manifestId -> bundleName; overrides the folder-name default
}
export async function generateComponentManifests(ctx: ManifestContext): Promise<ComponentManifest[]>; // reads <webpartsDir>/*/*.manifest.json
export async function generateManifestsJs(manifests: ComponentManifest[], metadata?: unknown): Promise<string>; // exact template from reference/FORMATS.md §3
export function findSpDependencies(projectRoot: string): Map<string, { id: string; version: string; manifestPath: string }>; // node_modules/@microsoft/sp-*/dist/*.manifest.json; fallback reference/sp-component-ids.json
export function rewriteSpManifestForDebug(spManifest: unknown, relativePath: string, baseUrl: string): unknown; // prepend base url per ManifestUrlProcessor@mbsks/rspfx-sppkg-builder (depends on core, diagnostics)
export interface PackageConfig { solution: Record<string, unknown>; paths: { zippedPackage: string } } // package-solution.json
export interface BuildPackageOptions {
projectRoot: string;
solutionConfigPath: string; // config/package-solution.json
manifestsDir: string; // release/manifests (prod) or dist (dev)
assetsDir: string; // release/assets or dist
outDir: string; // default projectRoot/sharepoint/solution (zippedPackage resolved against projectRoot)
production: boolean;
prettyXml?: boolean;
teamsDir?: string; // optional teams icons folder; files land under ClientSideAssets/ (flat)
resxDir?: string; // optional folder with Resources.resx + Resources.<lang>.resx (see below)
}
export async function buildPackage(opts: BuildPackageOptions): Promise<{ outputPath: string; zipEntries: string[]; appManifest: string }>;
// Zip via yazl or jszip (DEFLATE level 9). Full layout: reference/FORMATS.md §4. Must include:
// [Content_Types].xml, _rels/.rels → /AppManifest.xml, AppManifest.xml + _rels/AppManifest.xml.rels, feature_<id>.xml + .config.xml + _rels/feature_<id>.xml.rels,
// <featureId>/<ComponentType>_<componentId>.xml, ClientSideAssets feature + files (when includeClientSideAssets && production),
// rewrite manifest base urls to ['HTTPS://SPCLIENTSIDEASSETLIBRARY/'] when includeClientSideAssets.
// Extension components (componentType 'Extension'): elements XML uses Type="Extension", Location="ClientSideExtension.<extensionType>",
// ClientSideComponentProperties="null", and a ClientSideComponentInstance child (fresh random UUID per build); no <Module>.
// resxDir: Resources.resx (CultureName "default") + Resources.<lang>.resx parsed by src/resx.ts (regex, no deps; CultureName mapping in
// src/lcid.ts) and embedded at the zip root with content-defaultresource/content-resource rels; AppManifest metadata values
// "$Resources:Key" resolve into <LocalizedString CultureName="..."> entries per locale (missing keys fall back to the literal string).
export function validateSppkg(zipPath: string): Promise<{ ok: boolean; errors: string[] }>; // unzip + schema checks@mbsks/rspfx-manifest-server (depends on core, diagnostics)
manifest-server provides certs only; :4321 serving is handled by the compiler dev server (compiler-rspack startDevServer) (bundles, /temp/manifests.js, node_modules static proxy).
export async function ensureCertificates(certsDir: string, hostname?: string): Promise<{ key: string; cert: string }>;
// selfsigned (localhost + 127.0.0.1 + ::1 SANs + optional hostname, 825 days, 2048-bit, sha256);
// cached in certsDir (~/.rspfx/certs); validates via X509Certificate (expiry <7d, SAN mismatch);
// writes cert.pem.trust.txt + logs trust instructions and CORS warning on first generation
export function validateCustomHostname(hostname: string): void;
// allowlist for custom SAN hostname (rejects .., sharepoint suffix, injection chars)
export function getCertsDir(): string;
// → path.join(os.homedir(), '.rspfx', 'certs')
export interface CertStatus {
exists: boolean; keyExists: boolean; certExists: boolean; valid: boolean;
expiresAt?: string; daysUntilExpiry?: number; hostnameMismatch?: boolean; detail?: string;
}
export async function getCertStatus(certsDir: string, hostname?: string): Promise<CertStatus>;
// reads cert.pem via X509Certificate, checks expiry and SAN
export async function isCertTrusted(certPath: string): Promise<{ trusted: boolean | 'unknown'; detail: string }>;
// best-effort OS check: `security verify-cert` (macOS), `certutil -verify` (Windows), unknown on Linux
export function formatTrustInstructions(certsDir: string): string;
// per-OS trust command for ~/.rspfx/certs/cert.pem@mbsks/rspfx-dev-runtime (depends on core, compiler-rspack, manifest-server, manifest-generator, diagnostics, plugin-api, sharepoint-runtime, framework-*)
export type ServeMode = 'local' | 'sharepoint';
export interface DevRuntimeOptions {
projectRoot: string;
config: RspfxConfig;
fastRefresh?: boolean; // --refresh flag
noBrowser?: boolean; // inverse of --browser / dev.openBrowser
port?: number; // --port flag
tenantDomain?: string; // --tenant, config.dev.tenantUrl, or env var (see [docs/commands.md#rspfx-dev](commands.md#rspfx-dev) and AGENTS.md:47)
mode?: ServeMode; // --mode flag; default 'sharepoint' when a tenant domain is configured, else 'local'
}
export interface DevRuntimeHandle {
url: string; // dev server origin; http:// in local mode, https:// otherwise
port: number;
workbenchUrl: string | undefined; // undefined in local mode — there is no workbench URL
close(): Promise<void>;
}
export async function startServe(opts: DevRuntimeOptions): Promise<DevRuntimeHandle>;
export interface AssembleReleaseOptions {
projectRoot: string;
config: RspfxConfig;
project: ReadProjectResult;
externals: string[];
outputFiles: string[];
production: boolean;
}
export interface ReleaseOutput {
manifests: ComponentManifest[];
distDir: string;
releaseDir: string;
releaseManifestsDir: string;
releaseAssetsDir: string;
outputFiles: string[];
}
export async function assembleRelease(opts: AssembleReleaseOptions): Promise<ReleaseOutput>;
// Generates production component manifests (cdnBasePath from `config/write-manifests.json`) and assembles `release/manifests/*.manifest.json` + `release/assets/*` from `dist/`.
// Shared by `rspfx build` and native bundler commands (`vite build`, `rspack build`, `rsbuild build`); fires `releaseHooks.beforeGenerate` / `afterGenerate` identically.
// `mode 'local'` details: plain HTTP, sp-* externals emptied and bundled, extra `local-runtime` bundle compiled, local preview at `/` and mock `/_api` mounted.
export function resolveServeMode(opts: { mode?: ServeMode; config: RspfxConfig }, tenantDomain: string | undefined): ServeMode;
// explicit --mode wins; otherwise a configured tenant domain selects 'sharepoint', else 'local'
export function resolveServeSettings(opts: { port?: number; tenantDomain?: string; config: RspfxConfig }, serveJson: ProjectServeConfigJson | undefined): ServeSettings;
// port/hostname/https/tenant from CLI overrides → config/serve.json → plugin dev options → defaults (4321, localhost, https, scheme/origin derived)
export function buildWorkbenchUrl(settings: ServeSettings, config: RspfxConfig): string | undefined;
// <tenantUrl>/_layouts/15/workbench.aspx?debug=true&noredir=true&debugManifestsFile=<enc>.../temp/manifests.js
// undefined when no tenant domain is available; honors config.dev.workbench and initialPage ({tenantdomain} token)
export function stripScheme(url: string | undefined): string | undefined; // strips https?:// prefix and trailing slashes
export interface ServeSettings { port: number; hostname: string; https: boolean; scheme: string; origin: string; tenantDomain: string | undefined; initialPage: string | undefined }
export function readProject(projectRoot: string, paths?: PathsConfig, versionOverride?: string): ReadProjectResult;
// reads package.json/config.json/serve.json, discovers web parts and extensions; the version override
// (plugin `version` option) replaces package.json version in AMD library names and manifests
// discovery: explicit config.json bundles win; otherwise scans src/webparts/* and src/extensions/*
// (each folder needs one *.manifest.json + a pickable entrypoint: index, <name>WebPart,
// <name>ApplicationCustomizer/<name>FieldCustomizer/<name>CommandSet/<name>Extension, or a lone .ts/.tsx)
export function discoverWebParts(projectRoot: string, configJson?: ProjectConfigJson, webpartsDir?: string, packageJson?: { version?: string }, extensionsDir?: string): DiscoveredWebParts; // same scan, exported for tooling; discoverComponents is an alias
export function createReloadController(): ReloadController;
// dev auto-reload: monotonically increasing build counter served at /__rspfx_hot.json
// (no-store + CORS); tick() after each completed rebuild; clientScript is appended to
// /temp/manifests.js and polls the endpoint, calling location.reload() when the counter changes
export function createManifestRegenerator(opts: ManifestRegeneratorOptions): ManifestRegenerator;
// regenerates /temp/manifests.js after each compiler rebuild (project + sp-* debug manifests)
export interface RefreshRuntime {
dispose(): void;
preserveState(): void;
restoreState(): void;
readonly preserved: boolean;
readonly disposed: boolean;
readonly epoch: number; // completed preserve→restore cycles
}
export function createRefreshRuntime(framework: FrameworkId, options?: {
onPreserve?: () => void; onRestore?: () => void;
}): RefreshRuntime;
// framework-agnostic state machine; created only when fast refresh is enabled;
// wired into startServe's manifest-regeneration cycle (preserveState before,
// restoreState in finally, dispose on close)
export interface FrameworkPresetModule {
preset: FrameworkPreset; // loaded preset (no-op when the framework package is missing)
moduleUrl: string; // resolved framework package path ('' when missing)
}
export async function loadFrameworkPreset(framework: FrameworkId, projectRoot?: string): Promise<FrameworkPresetModule>;
// imports @mbsks/rspfx-framework-<fw> index (Node side); warns + returns a no-op preset when absent
export function resolveContributionLoaders(contributions: Record<string, unknown>, frameworkModuleUrl: string): Record<string, unknown>;
// resolves bare loader strings ('vue-loader', 'svelte-loader', 'babel-loader') and babel
// preset/plugin strings ('babel-preset-solid') in rule `use` entries against the framework
// package's own node_modules (createRequire from moduleUrl); unchanged when moduleUrl is ''Local preview internals (module-level exports in src/, used by startServe in mode: 'local'; not re-exported from the package index):
src/local-page.ts—buildLocalPageHtml(opts: LocalPageOptions): string: static HTML served at/— injects discovered web part list intowindow.__RSPFX_COMPONENTS__, loads/dist/local-runtime.js, appends reload client script.readLocalPageComponents(bundles, packageVersion): LocalPageComponent[]derives{ id, alias, bundleName, amdId, preconfiguredEntries }from manifests.src/mock-api.ts—createMockSharePointApi(opts: { projectRoot: string; origin: () => string })returns{ path: '/_api', handle(req, res) }: mock SharePoint REST API (OData v4 JSON-light,/_api/web,/site,/lists, item CRUD viaX-HTTP-Method,POST /contextinfo, 404/400 envelopes). Store seeded fromcreateDefaultMockStore()and optionally overridden bylocal/data.json.
@mbsks/rspfx-framework-* (depends on core, plugin-api, webpart-base; peer: framework libs)
Each package @mbsks/rspfx-framework-react|solid|preact|vue|svelte|vanilla exposes three entry points:
- Index (
@mbsks/rspfx-framework-<fw>) — Node-safe; exports only the preset, never imports@mbsks/rspfx-webpart-base:
export const preset: FrameworkPreset; // name = '<framework>'- Subpath (
@mbsks/rspfx-framework-<fw>/headless) — pure adapter factory, no SPFx dependency:
import { createReactAdapter } from '@mbsks/rspfx-framework-react/headless';
export function createReactAdapter<TProps>(renderComponent: (props: TProps) => ReactNode): HeadlessAdapter<TProps>;
import { createSolidAdapter } from '@mbsks/rspfx-framework-solid/headless';
export function createSolidAdapter<TProps>(renderComponent: (props: TProps) => JSX.Element): HeadlessAdapter<TProps>;
import { createVanillaAdapter } from '@mbsks/rspfx-framework-vanilla/headless';
export function createVanillaAdapter<TProps>(render: (props: TProps) => HTMLElement | string): HeadlessAdapter<TProps>;
import { createVueAdapter } from '@mbsks/rspfx-framework-vue/headless';
export function createVueAdapter<TProps>(factory: (props: TProps) => Component): HeadlessAdapter<TProps>;
import { createPreactAdapter } from '@mbsks/rspfx-framework-preact/headless';
export function createPreactAdapter<TProps>(renderComponent: (props: TProps) => ComponentChild): HeadlessAdapter<TProps>;
import { createSvelteAdapter } from '@mbsks/rspfx-framework-svelte/headless';
export function createSvelteAdapter<TProps>(factory: (props: TProps) => SvelteWebPartComponent<TProps>): HeadlessAdapter<TProps>;- Subpath (
@mbsks/rspfx-framework-<fw>/webpart) — thinHeadlessWebPartshim (deprecated, kept for one major):
import { ReactWebPart } from '@mbsks/rspfx-framework-react/webpart';
export abstract class <Cap>WebPart<TProps, TState> extends HeadlessWebPart<TProps> {
protected abstract renderComponent(props: TProps): unknown;
protected createAdapter(): HeadlessAdapter<TProps>; // delegates to createXAdapter
}- Svelte's
/headlessalso exportsSvelteWebPartComponent<TProps>— the{ component, props }shape. framework-vanilla:createVanillaAdaptermountusesreplaceChildren; no refresh.- React:
createReactAdaptercachesRootinWeakMap<HTMLElement,Root>; re-render viaroot.render; fast refresh contributors unchanged. - Solid:
createSolidAdapterusesrender(() => comp, root)with per-adapterWeakMap<HTMLElement,()=>void>; refresh unchanged. - Preact:
createPreactAdapterusesrender(vnode, root)/render(null, root). - Vue:
createVueAdapterusescreateApp(...).mountwithWeakMap<HTMLElement,App>. - Svelte:
createSvelteAdapterdoesnew Component({ target: root, props })withWeakMap<HTMLElement,SvelteComponentTyped>; checks$destroyexistence for Svelte 4/5 compat. - Each preset's
contributionsunchanged;vite()/rsbuild()methods unchanged.
@mbsks/rspfx-sharepoint-runtime (depends on core, diagnostics; peer @microsoft/sp-*)
Local preview emulation of the SPFx client — used by the /dist/local-runtime.js bootstrap (rspfx dev local mode) to instantiate web parts without SharePoint:
export function createLocalWebPartContext(
manifest: unknown,
overrides?: Record<string, unknown>,
options?: CreateLocalContextOptions
): Promise<WebPartContextLike>;
// Builds a REAL WebPartContext (`new WebPartContext(parameters)`, mirroring
// ClientSideWebPartManager._getWebPartContext) over a parent ServiceScope that
// provides a mock PageContext + a local theme provider under the real service
// keys (PageContext.serviceKey / ThemeProvider.serviceKey). The REAL
// SPHttpClient/HttpClient are kept (they work against the dev server); the
// child-scope MSGraphClientFactory / AadHttpClientFactory / AadTokenProviderFactory
// registrations are replaced with mocks. Environment.type is Local.
// options.services can override spHttpClient/msGraphClientFactory/aadHttpClientFactory/
// pageContext/themeProvider; options.createScope/createContext are test seams.
export function createMockPageContextData(overrides?: Partial<LocalPageContextData>): LocalPageContextData;
export const LOCAL_CURRENT_USER: Record<string, unknown>;
export type { CreateLocalContextOptions, LocalContextServices, LocalPageContextData, ScopeLike };
export { createMockThemeProvider, LOCAL_THEMES } from './theme'; // Fluent-faithful light/dark themes
// LocalThemeProvider: tryGetTheme()/getTheme()/themeChangedEvent (add/remove)/setTheme()/dispose()
export { createMockSPHttpClient, createMockAadHttpClientFactory, createMockMSGraphClientFactory,
defaultMockTransport, LOCAL_GRAPH_DATA } from './http'; // mocked graph/AAD data clients
// LEGACY — retained for compat, used nowhere at runtime (docs mark these, do not remove):
export const PLAYGROUND_SERVICE_KEY = '__rspfx_playground__'; // self-referenced only by its own test
export function createMockWebPartContext(manifest: unknown, overrides?: Record<string, unknown>): WebPartContextLike; // pre-emulation flat mock
export function createPlaygroundLoader(mountComponent: (root: HTMLElement) => void, unmountComponent?: (root: HTMLElement) => void): { mount(root: HTMLElement): void; unmount(): void };Subpath @mbsks/rspfx-sharepoint-runtime/local-bootstrap — the browser entry compiled by the dev server as the local-runtime bundle: installs global AMD define/require hooks on window, initializes Environment type to Local, reads window.__RSPFX_COMPONENTS__, loads each web part bundle by amdId, and per component: createLocalWebPartContext → _internalInitialize(context, false, DisplayMode.Read) → _internalDeserialize({ properties, dataVersion }) → await onInit() → render().
@mbsks/rspfx-fluent-adapter (depends on core, framework-react, webpart-base; peer @fluentui/react)
export class FluentWebPart<TProps, TState> extends HeadlessWebPart<TProps> {
protected onThemeChanged(): void; // syncs context.themeProvider → Fluent ThemeProvider
}
export function createFluentAdapter<TProps>(render: (props: TProps, theme: ITheme) => ReactNode, getThemeProvider?: () => ThemeProvider, getTheme?: () => ITheme): HeadlessAdapter<TProps>;@mbsks/rspfx-templates (depends on core)
export interface TemplateVars {
name: string; // project or webpart name (kebab-case)
namePascal: string; // PascalCase
nameCamel: string;
framework: FrameworkId;
spfxVersion: SpfxTarget;
language: 'typescript' | 'javascript';
tenantUrl?: string;
componentId: string; // uuid for webpart manifest
solutionId: string; // uuid for package-solution.json
featureId: string; // uuid
packageName: string; // npm name
packageVersion: string;
}
export function scaffoldProject(vars: TemplateVars, destDir: string): Promise<string[]>; // returns written file pathsGenerated project layout:
package.json tsconfig.json rspack.config.ts .gitignore .npmrc README.md
config/package-solution.json config/serve.json config/write-manifests.json
sharepoint/assets/.gitkeep
src/index.ts src/rspfx-env.d.ts
src/webparts/<name>/<name>.manifest.json <name>WebPart.ts components/<Pascal>.tsx|ts|vue|svelte styles/<Pascal>.module.scss|css assets/.gitkeep(Exact scaffold file contents are the template's design; must match API usage below.)
There is no playground/ folder in the scaffold — the local preview page is generated by dev-runtime (local-page.ts) and served at / by rspfx dev (--mode local, the default). The scaffolded <name>WebPart.ts extends BaseClientSideWebPart from @microsoft/sp-webpart-base directly (no @mbsks/rspfx-framework-* web part base).
@mbsks/rspfx-cli (depends on ALL packages)
Bin rspfx. Commands (commander):
rspfx new <name>— interactive prompts (framework, language, spfx target, pm); flags--framework <id> --language <ts|js> --spfx-version <v> --pm <pnpm|npm|yarn|bun> --no-install --yesfor non-interactive. Then scaffold +git init(no dependency install — runpnpm|npm|yarn|bun installyourself).rspfx dev— startServe; flags--refresh,--browser,--port <n>,--mode <local|sharepoint>,--tenant <url>rspfx build— production compile to dist + release (manifests/assets);--no-minify --sourcemaprspfx package— build + package → sppkg;--no-buildrspfx deploy— package + upload to app catalog (see docs/commands.md#rspfx-deploy and AGENTS.md:47 for env vars; URL validated, 120s upload timeout); prints manual steps without a tokenrspfx analyze— build + bundle report (sizes, chunk list) to.rspfx/analyze.html+ console table; module counts from bundler stats (Rspack) or the.rspfx/stats.jsonfallback (Vite/Rsbuild)rspfx doctor— env/config/ports/deps checks, exit code 1 on failuresrspfx clean— rm dist release temp .rspfx node_modules/.cacherspfx --version,rspfx --help
Config loading: jiti import of rspack.config.ts (or vite.config.ts, or rsbuild.config.ts), find the plugin by RSPFX_PLUGIN_MARKER, read .options → tryResolveConfig → Result<RspfxConfig, Issue[]>. Guidance error when no config or no plugin is found.
LoadedProject is loadConfig(projectRoot) → { config: RspfxConfig, bundler: BundlerId, configFile: string, plugin: RspfxBundlerPluginLike, bundlerConfig: unknown, rspfx: RspfxInstance, userModuleRules?: readonly unknown[] }. Hybrid mode (apps/cli/src/hybrid.ts, see docs/hybrid-dev.md): detectOfficialProject(projectRoot) → { toolchainMarker } | undefined (requires config/config.json + gulpfile.js/heft.json/.yo-rc.json); loadOfficialConfig(projectRoot) → synthesized RspfxConfig (name/version from package.json, spfxVersion from the @microsoft/sp-core-library dependency, framework from dependency scan); used by runDev on CONFIG_NOT_FOUND. loadConfigOrRefuseOfficial(projectRoot) replaces loadConfig in build/package/deploy/analyze and throws RspfxError('OFFICIAL_TOOLCHAIN_BUILD') on official projects. Per-command bundler awareness: for Vite configs, dev/build/package spawn the project-local vite/vite build (one build per web part bundle); for Rsbuild configs a single rsbuild build runs and dev spawns rsbuild dev; for Rspack configs the internal Rspack pipeline runs as before. On Vite/Rsbuild projects dev --refresh (or dev.fastRefresh) sets RSPFX_FAST_REFRESH=1 on the spawned process. The local preview page and mock /_api API are served by dev-runtime's startServe on the Rspack path — the Vite/Rsbuild dev flows are workbench-only for now. The Rsbuild plugin mirrors the Vite dev features: serves /temp/manifests.js + the /__rspfx_hot.json reload counter via onBeforeStartDevServer middlewares, regenerates manifests and ticks the counter on onAfterDevCompile, keeps dev unminified (optimization.minimize: false), and opens the workbench when dev.openBrowser is set (via onAfterStartDevServer). Guid generation: crypto.randomUUID.