Repository navigation
review: full ComponentOnce repository from empty base #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
swittk
wants to merge
1
commit into
premerge/empty-componentonce
from
review/componentonce-full-repo
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| node_modules/ | ||
| dist/ | ||
| dist-cjs/ | ||
| coverage/ | ||
| .artifacts/ | ||
| *.tsbuildinfo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 22 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # ComponentOnce engineering rules | ||
|
|
||
| - ComponentOnce is intended to be publishable FOSS. Core must not depend on any host application, database, filesystem, HTTP transport, visual editor, or product-specific runtime. | ||
| - Keep three concepts distinct: persisted/configured component Props, arbitrary host-owned runtime Context, and arbitrary per-render Payload. | ||
| - Context and Payload are generic host types. Core must never assume their shape. | ||
| - Exact module id + version is deterministic. Hosts must be able to pin a version rather than silently taking latest. | ||
| - Registry/load/compiler/storage are separate contracts. A registry does not know where source/bundles are stored. | ||
| - Core must not import React. React integration belongs in @componentonce/react. | ||
| - Compiler implementation belongs outside core. @componentonce/compiler-esbuild is optional tooling. | ||
| - Dynamic/trusted-code loading is the v0 target. Do not add marketplace/sandbox policy until a host actually needs it. | ||
| - Compiled React modules must use the host React instance. Never silently bundle a second React copy into a dynamically loaded component. | ||
| - Adapters own I/O and repeated validation. Core should stay small, deterministic, and boring. | ||
| - No hidden global singleton registry. Hosts may run multiple isolated registries. | ||
| - Every exported named type/interface/class/function gets a short purpose doc comment. | ||
| - Public API changes require tests and a consumer-style example. | ||
| - When multiple contributors work concurrently, coordinate ownership before editing overlapping files. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # ComponentOnce | ||
|
|
||
| Small FOSS-ready contracts for registering, loading, packaging, and rendering trusted, versioned, application-specific components. | ||
|
|
||
| ComponentOnce keeps three values distinct: | ||
|
|
||
| - **Props** are persisted/configured component values. | ||
| - **HostContext** is an arbitrary rich runtime object owned by the host application. | ||
| - **Payload** is arbitrary application/domain data supplied for one render or mount. | ||
|
|
||
| ## Quick React flow | ||
|
|
||
| A component author writes an ordinary typed definition: | ||
|
|
||
| ```tsx | ||
| import { | ||
| createReactRequirement, | ||
| defineReactComponent, | ||
| } from "@componentonce/react"; | ||
|
|
||
| interface Props { | ||
| readonly title: string; | ||
| } | ||
|
|
||
| interface HostContext { | ||
| readonly format: (value: number) => string; | ||
| } | ||
|
|
||
| interface Payload { | ||
| readonly total: number; | ||
| } | ||
|
|
||
| export const definition = defineReactComponent<Props, HostContext, Payload>({ | ||
| manifest: { | ||
| id: "example/total-card", | ||
| version: "1.0.0", | ||
| requirements: [createReactRequirement("19.3.0")], | ||
| }, | ||
| component({ props, context, payload }) { | ||
| return <strong>{props.title}: {context.format(payload.total)}</strong>; | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| The React requirement is explicit and persisted. Choose the exact version, or a stable version convention, that your host compatibility policy understands. | ||
|
|
||
| Build a self-describing package: | ||
|
|
||
| ```sh | ||
| componentonce build ./src/total-card.tsx \ | ||
| --renderer react \ | ||
| --out ./dist/total-card.componentonce.json | ||
| ``` | ||
|
|
||
| The package contains the manifest, integrity-protected executable bundle, and any CSS, images, fonts, or configured file-loader outputs reached through ordinary relative imports. A catalog can read `package.manifest` without executing component code. The trusted high-level builder evaluates the module once during build to discover that manifest. | ||
|
|
||
| Application integrations bind the host context type and runtime policy once; each component still owns its Props and Payload types: | ||
|
|
||
| ```ts | ||
| import { createReactHost } from "@componentonce/react"; | ||
|
|
||
| const components = createReactHost<HostContext>({ | ||
| capabilities: [{ name: "example-api", version: "1" }], | ||
| }); | ||
|
|
||
| const element = components.render({ | ||
| definition, | ||
| props: { title: "Total" }, | ||
| context, | ||
| payload, | ||
| }); | ||
| ``` | ||
|
|
||
| The React adapter automatically advertises the actual peer React singleton/version. Callers do not repeat React or application capabilities on every render. | ||
|
|
||
| ## Packages | ||
|
|
||
| ### `@componentonce/core` | ||
|
|
||
| Renderer-agnostic exact identities, versioned registries, loader contracts, validators, and generic runtime/application capability requirements. It imports no React, compiler, storage, transport, database, or application code. | ||
|
|
||
| ### `@componentonce/react` | ||
|
|
||
| React definitions and rendering using the host React singleton, boundary validation, automatic React capability reporting, and host-bound helpers. | ||
|
|
||
| ### `@componentonce/dom` | ||
|
|
||
| React-free ordinary DOM components with explicit `mount -> update -> destroy` lifecycle, automatic DOM capability reporting, and host-bound helpers. | ||
|
|
||
| ### `@componentonce/runtime` | ||
|
|
||
| Browser-safe trusted package loading and execution. It parses the same self-describing package envelope emitted by compiler adapters, verifies executable and embedded asset bytes with Web Crypto SHA-256, prepares host-resolved asset URLs, attaches styles to an explicit `Document` or `ShadowRoot`, and evaluates trusted bundles with an explicit host-external map. It imports no compiler or Node builtin. | ||
|
|
||
| ### `@componentonce/compiler-esbuild` | ||
|
|
||
| Optional trusted build tooling. It supports low-level compilation plus self-describing package helpers and the `componentonce build` CLI. Relative JavaScript, CSS, CSS Module, and static-file imports can be bundled; package/runtime imports stay explicit host externals. | ||
|
|
||
| ### `@componentonce/dev` | ||
|
|
||
| Optional local React development workbench. `componentonce dev` watches the full source/import/asset graph through the production compiler, executes the result against a browser-only real host profile and shared React singleton, keeps Props/Context/Payload fixtures separate, resolves host-defined actual/mock callable references into real function-valued Props/Payload while logging invocations, retains the last good preview across compile failures, and can export an ordinary v2 package. It defaults to loopback-only trusted-code tooling, with explicit configurable interface binding for LAN/device testing; it is not a production server or sandbox. | ||
|
|
||
| ## Compatibility | ||
|
|
||
| A manifest can require multiple named capabilities, for example React plus an application SDK. Exact name/version equality is the default. Hosts can supply another compatibility predicate when they intentionally support ranges or other version policies. | ||
|
|
||
| Sharing the host React singleton prevents duplicate-React hook failures. Capability checks separately prevent a component authored against an incompatible React/API version from rendering accidentally. | ||
|
|
||
| ## Trust model | ||
|
|
||
| The v0 target is trusted internal component code. The esbuild evaluator uses `new Function`; it is not a security sandbox. Storage, transport, databases, visual editors, and marketplace policy remain host concerns. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "name": "componentonce", | ||
| "private": true, | ||
| "version": "0.1.0-beta.2", | ||
| "type": "module", | ||
| "packageManager": "pnpm@10.20.0", | ||
| "engines": { | ||
| "node": ">=20" | ||
| }, | ||
| "scripts": { | ||
| "check": "pnpm -r check", | ||
| "test": "pnpm -r test", | ||
| "build": "pnpm -r build" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24.10.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.5" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| # @componentonce/compiler-esbuild | ||
|
|
||
| Optional trusted compiler, packager, and evaluator tooling for ComponentOnce. | ||
|
|
||
| This package is not part of the core runtime ABI. It owns no storage or transport. | ||
|
|
||
| ## CLI: source file to self-describing package | ||
|
|
||
| Install the renderer adapter you use plus this compiler, then: | ||
|
|
||
| ```sh | ||
| componentonce build ./src/amount-card.tsx \ | ||
| --renderer react \ | ||
| --out ./dist/amount-card.componentonce.json | ||
| ``` | ||
|
|
||
| The default renderer is `react`, so this is also valid: | ||
|
|
||
| ```sh | ||
| componentonce build ./src/amount-card.tsx | ||
| ``` | ||
|
|
||
| Useful options: | ||
|
|
||
| ```text | ||
| --renderer <id> Renderer id recorded in the package | ||
| -o, --out <file> Output JSON package | ||
| --export <name> Definition export name (default: definition) | ||
| --external <name> Additional host module kept external and loaded for trusted build discovery | ||
| --loader <ext=kind> Additional esbuild loader, for example .bin=file or .svg=dataurl | ||
| --content-type <ext=type> Media type override for an emitted file-loader asset | ||
| ``` | ||
|
|
||
| React builds automatically externalize `react`, the JSX runtimes, and `@componentonce/react`. DOM builds automatically externalize `@componentonce/dom`. Relative imports are bundled from the entry file directory; undeclared package imports still fail rather than being silently pulled from the compiler process. | ||
|
|
||
| Ordinary relative static imports are packaged automatically: | ||
|
|
||
| ```tsx | ||
| import "./card.css"; | ||
| import styles from "./card.module.css"; | ||
| import logoUrl from "./logo.svg"; | ||
|
|
||
| // CSS may use @import and url("./font.woff2"). | ||
| ``` | ||
|
|
||
| CSS file references may omit `./` and may include query or fragment suffixes; the emitted package keeps the suffix while resolving the exact file bytes. Root-relative and remote CSS asset URLs are rejected because they are not portable package assets. Explicit `data:` URLs remain inline. | ||
|
|
||
| Common image and font extensions use esbuild's `file` loader by default. Configure any other extension instead of relying on a fixed media whitelist: | ||
|
|
||
| ```sh | ||
| componentonce build ./src/card.tsx \ | ||
| --loader .mesh=file \ | ||
| --content-type .mesh=application/vnd.example.mesh | ||
| ``` | ||
|
|
||
| Use `--loader .svg=dataurl` only when deliberately inlining a small file. The default `file` path keeps binary bytes out of executable JavaScript. JSON and `.txt` retain esbuild's normal `json` and `text` behavior. | ||
|
|
||
| The source module normally exports one definition: | ||
|
|
||
| ```tsx | ||
| import { | ||
| createReactRequirement, | ||
| defineReactComponent, | ||
| } from "@componentonce/react"; | ||
|
|
||
| export const definition = defineReactComponent<Props, HostContext, Payload>({ | ||
| manifest: { | ||
| id: "example/amount-card", | ||
| version: "1.0.0", | ||
| requirements: [createReactRequirement("19.3.0")], | ||
| }, | ||
| component({ props, context, payload }) { | ||
| return <strong>{props.label}: {context.formatAmount(payload.amount)}</strong>; | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| The high-level builder evaluates trusted source once during build to discover the exported manifest. The resulting package stores that manifest next to the bundle, so catalogs can inspect identity, version, and requirements without executing the component. | ||
|
|
||
| ## Programmatic packaging | ||
|
|
||
| ```ts | ||
| import * as React from "react"; | ||
| import * as jsxRuntime from "react/jsx-runtime"; | ||
| import * as ComponentOnceReact from "@componentonce/react"; | ||
| import { | ||
| buildTrustedReactPackage, | ||
| serializeTrustedComponentPackage, | ||
| } from "@componentonce/compiler-esbuild"; | ||
|
|
||
| const componentPackage = await buildTrustedReactPackage({ | ||
| source, | ||
| sourceFileName: "amount-card.tsx", | ||
| resolveDir: sourceDirectory, | ||
| externals: { | ||
| react: React, | ||
| "react/jsx-runtime": jsxRuntime, | ||
| "@componentonce/react": ComponentOnceReact, | ||
| }, | ||
| loaders: { ".mesh": "file" }, | ||
| contentTypes: { ".mesh": "application/vnd.example.mesh" }, | ||
| }); | ||
|
|
||
| await store.put( | ||
| componentPackage.manifest.id, | ||
| serializeTrustedComponentPackage(componentPackage), | ||
| ); | ||
| ``` | ||
|
|
||
| At runtime use `@componentonce/runtime`, so a browser does not import esbuild or Node builtins: | ||
|
|
||
| ```ts | ||
| import { | ||
| createBrowserBlobAssetUrlResolver, | ||
| instantiateTrustedComponentPackage, | ||
| parseTrustedComponentPackage, | ||
| prepareTrustedComponentPackageAssets, | ||
| } from "@componentonce/runtime"; | ||
|
|
||
| const componentPackage = parseTrustedComponentPackage(await store.get(...)); | ||
|
|
||
| console.log(componentPackage.manifest); // no component execution | ||
|
|
||
| if (componentPackage.format !== "componentonce.trusted-package.v2") { | ||
| throw new Error("Expected an asset-capable package"); | ||
| } | ||
| const blobs = createBrowserBlobAssetUrlResolver(); | ||
| const assets = await prepareTrustedComponentPackageAssets(componentPackage, { | ||
| resolveAssetUrl: blobs.resolveAssetUrl, | ||
| releaseAssetUrl: blobs.releaseAssetUrl, | ||
| }); | ||
| const definition = await instantiateTrustedComponentPackage(componentPackage, { | ||
| externals: { | ||
| react: React, | ||
| "react/jsx-runtime": jsxRuntime, | ||
| "@componentonce/react": ComponentOnceReact, | ||
| }, | ||
| preparedAssets: assets, | ||
| }); | ||
|
|
||
| const styleMount = assets.mountStyles(container.ownerDocument); | ||
| // Render or mount the definition into container. | ||
| styleMount.release(); | ||
| assets.dispose(); | ||
| blobs.dispose(); | ||
| ``` | ||
|
|
||
| Preparation verifies the executable bundle and every embedded asset before producing URLs or styles. Instantiation verifies that the definition inside the executable bundle still has the same manifest as the package envelope. | ||
|
|
||
| The v2 package has sorted `assets` entries with safe relative `path`, `contentType`, exact byte length, SHA-256/SRI values, and base64 bytes. `stylesheets` contains exact references into that table. Generated `componentonce-asset:` tokens are the only strings the runtime resolves. Node tooling may instantiate with only `{ externals }` for manifest/build inspection; file imports remain deliberate tokens until a URL resolver is supplied. Existing v1 packages without assets remain readable and executable. | ||
|
|
||
| Plain `.css` is global CSS. It is not automatically isolated. Prefer `.module.css` for scoped class names; ComponentOnce adds a deterministic namespace derived from the portable input/dependency graph, so changing a referenced image, font, or imported style also changes local class names while identical packages in different absolute directories remain reproducible. A host may instead mount styles in a `ShadowRoot`, but Shadow DOM is never forced. | ||
|
|
||
| Build output is independent of the process working directory. Absolute `sourceFileName` values are reduced to their basename, and build-machine paths are not persisted in bundle comments, source maps, or metafile data. | ||
|
|
||
| ## Low-level compilation | ||
|
|
||
| `compileTrustedModule` compiles JavaScript/TypeScript into an immutable CommonJS artifact. Caller-declared package imports remain external. Relative imports can be bundled when `resolveDir` is supplied. | ||
|
|
||
| `compileTrustedReactModule` adds automatic JSX transformation and React/JSX host externals while still never bundling React. | ||
|
|
||
| Artifacts contain deterministic bundle text, byte length, SHA-256 hash/integrity, normalized diagnostics, exact external imports, and esbuild metafile data. | ||
|
|
||
| ## Trust model | ||
|
|
||
| `instantiateTrustedBundle` deliberately uses `new Function` with a narrow injected require map. It is for trusted internal code and is not a security sandbox. | ||
|
|
||
| esbuild transpiles TypeScript syntax but does not perform semantic type checking; hosts that need that guarantee should run their TypeScript checker separately. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| @font-face { | ||
| font-family: "Asset Card Sans"; | ||
| src: url("./asset-card.woff2") format("woff2"); | ||
| } | ||
|
|
||
| .card { | ||
| display: flex; | ||
| gap: 0.75rem; | ||
| font-family: "Asset Card Sans", sans-serif; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { createReactRequirement, defineReactComponent } from "@componentonce/react"; | ||
| import styles from "./asset-card.module.css"; | ||
| import logoUrl from "./asset-logo.svg"; | ||
|
|
||
| interface AssetCardProps { | ||
| readonly title: string; | ||
| } | ||
|
|
||
| /** Example React definition using an embedded CSS Module, SVG, and font URL. */ | ||
| export const definition = defineReactComponent<AssetCardProps, unknown, unknown>({ | ||
| manifest: { | ||
| id: "example/asset-card", | ||
| version: "1.0.0", | ||
| requirements: [createReactRequirement("19.3.0")], | ||
| }, | ||
| component({ props }) { | ||
| return ( | ||
| <article className={styles.card}> | ||
| <img src={logoUrl} alt="" /> | ||
| <strong>{props.title}</strong> | ||
| </article> | ||
| ); | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| example-font-placeholder |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| declare module "*.module.css" { | ||
| const classes: Readonly<Record<string, string>>; | ||
| export default classes; | ||
| } | ||
|
|
||
| declare module "*.svg" { | ||
| const url: string; | ||
| export default url; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.