feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
Makes the public module-development package set usable from a module repository outside the monorepo. Nothing is published. - Curated @ekklesia/module-sdk: an explicit author API, no contracts barrel, no identity/authorization internals, no Drizzle and no legacy ModuleDatabase type. defineModuleRuntime returns the SDK's own named interfaces so a module's emitted declarations never reach into @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and defineModuleSchemaMetadata live in @ekklesia/module-database. - Drizzle: no longer a dependency of module-sdk or contracts. Core keeps the legacy Drizzle schema validator (moved to core/src/modules) and its legacy runtime support; drizzle-orm is an optional peer only. - React is a peer (plus devDependency) of @ekklesia/ui; the boundary checker allows exactly that consumer-selected peer. - @ekklesia/typescript-config is publishable; module-ui-host is private. Every public package carries "ekklesia.distribution": "module-development" and no monorepo-root prepack. - module-pack reads the Node.js range from the module's own engines.node and recognizes Ekklesia packages by name plus that marker, so registry-installed packages are incorporated, SDK-compatibility checked, and never credited as third-party. - scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs and inspects the nine public tarballs; scripts/test-standalone-module.sh (pnpm test:standalone-module, new CI job) installs them into a fixture module outside the repository and proves it typechecks, builds, packages, and verifies. - Public-package boundary tests and module-development docs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9426c83109
commit
89d5d0f2da
@@ -20,10 +20,10 @@ import { build, type Message, type Plugin } from "esbuild";
|
||||
import { minVersion, satisfies, subset, validRange } from "semver";
|
||||
import {
|
||||
NATIVE_TARGETS,
|
||||
type NativePackage,
|
||||
type NativeTarget,
|
||||
NativeError,
|
||||
type NativeErrorCode,
|
||||
type NativePackage,
|
||||
type NativeTarget,
|
||||
nativeClosure,
|
||||
nativeRuntime,
|
||||
} from "./native.js";
|
||||
@@ -130,7 +130,7 @@ const messages: Readonly<Record<ModulePackErrorCode, string>> = Object.freeze({
|
||||
"A package path is not portable printable ASCII that fits the archive format.",
|
||||
MODULE_PACK_REVISION_INVALID: "The source revision must be a full 40- or 64-character commit id.",
|
||||
MODULE_PACK_RUNTIME_UNKNOWN:
|
||||
"The Node.js range could not be determined. Run inside an Ekklesia workspace, or pass one.",
|
||||
"The module's package.json must declare the Node.js versions it supports in engines.node.",
|
||||
MODULE_PACK_SDK_INCOMPATIBLE:
|
||||
"The bundled Module SDK is outside the manifest's own compatibility.moduleSdk range.",
|
||||
MODULE_PACK_TARGET_UNSUPPORTED:
|
||||
@@ -170,8 +170,7 @@ export interface PackModuleOptions {
|
||||
readonly sourceRevision: string;
|
||||
/** Informational only; recorded as given. */
|
||||
readonly repository?: string;
|
||||
/** The Node.js range the package is built for. Defaults to the enclosing workspace's
|
||||
* `engines.node`, the toolchain the module was built and tested with. */
|
||||
/** The Node.js range the package is built for. Defaults to the module's own `engines.node`. */
|
||||
readonly nodeRange?: string;
|
||||
/** Reviewed license supplements (ADR 0052). Defaults to the ones this packager ships. */
|
||||
readonly supplementsDirectory?: string;
|
||||
@@ -234,27 +233,16 @@ async function packagerIdentity(): Promise<{ readonly name: string; readonly ver
|
||||
return { name: String(manifest.name), version: String(manifest.version) };
|
||||
}
|
||||
|
||||
async function workspaceNodeRange(moduleDirectory: string): Promise<string> {
|
||||
let directory = moduleDirectory;
|
||||
for (;;) {
|
||||
try {
|
||||
await lstat(path.join(directory, "pnpm-workspace.yaml"));
|
||||
const manifest = await readJson(path.join(directory, "package.json"));
|
||||
const engines = isRecord(manifest) ? manifest.engines : undefined;
|
||||
const range = isRecord(engines) ? engines.node : undefined;
|
||||
if (typeof range === "string") {
|
||||
return range;
|
||||
}
|
||||
break;
|
||||
} catch {
|
||||
const parent = path.dirname(directory);
|
||||
if (parent === directory) {
|
||||
break;
|
||||
}
|
||||
directory = parent;
|
||||
}
|
||||
/** The Node.js range a module declares for itself, in its own package.json `engines.node` (ADR
|
||||
* 0053). Read from the module, never from an enclosing workspace: a standalone module repository has
|
||||
* none. */
|
||||
function moduleNodeRange(developmentPackage: Readonly<Record<string, unknown>>): string {
|
||||
const engines = developmentPackage.engines;
|
||||
const range = isRecord(engines) ? engines.node : undefined;
|
||||
if (typeof range !== "string" || range.trim() === "") {
|
||||
throw new ModulePackError("MODULE_PACK_RUNTIME_UNKNOWN");
|
||||
}
|
||||
throw new ModulePackError("MODULE_PACK_RUNTIME_UNKNOWN");
|
||||
return range;
|
||||
}
|
||||
|
||||
/** `./dist/server/index.js` → `dist/server/index.js`, inside the module directory. */
|
||||
@@ -537,7 +525,7 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
|
||||
throw new ModulePackError("MODULE_PACK_MODULE_LICENSE_FILE_INVALID");
|
||||
}
|
||||
|
||||
const nodeRange = options.nodeRange ?? (await workspaceNodeRange(moduleDirectory));
|
||||
const nodeRange = options.nodeRange ?? moduleNodeRange(developmentPackage);
|
||||
if (validRange(nodeRange) === null) {
|
||||
throw new ModulePackError("MODULE_PACK_RUNTIME_UNKNOWN");
|
||||
}
|
||||
@@ -624,14 +612,17 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
|
||||
if (bundled === undefined || bundled.root === moduleDirectory) {
|
||||
continue;
|
||||
}
|
||||
if (!bundled.thirdParty) {
|
||||
if (bundled.name.startsWith("@ekklesia/")) {
|
||||
const known = incorporates.get(bundled.name);
|
||||
if (known !== undefined && known !== bundled.version) {
|
||||
throw new ModulePackError("MODULE_PACK_INCORPORATED_CONFLICT", bundled.name);
|
||||
}
|
||||
incorporates.set(bundled.name, bundled.version);
|
||||
if (bundled.ekklesiaOwned) {
|
||||
// An Ekklesia module-development package, recognized by its identity, not by where it was
|
||||
// installed (ADR 0053): recorded under incorporates, never credited as third-party software.
|
||||
const known = incorporates.get(bundled.name);
|
||||
if (known !== undefined && known !== bundled.version) {
|
||||
throw new ModulePackError("MODULE_PACK_INCORPORATED_CONFLICT", bundled.name);
|
||||
}
|
||||
incorporates.set(bundled.name, bundled.version);
|
||||
continue;
|
||||
}
|
||||
if (!bundled.thirdParty) {
|
||||
continue;
|
||||
}
|
||||
if (use === "backend" && bundled.name === "drizzle-orm") {
|
||||
@@ -657,6 +648,7 @@ export async function packModule(options: PackModuleOptions): Promise<ModulePack
|
||||
manifest: nativePackage.manifest,
|
||||
name: nativePackage.name,
|
||||
root: nativePackage.root,
|
||||
ekklesiaOwned: false,
|
||||
thirdParty: true,
|
||||
version: nativePackage.version,
|
||||
},
|
||||
|
||||
@@ -3,18 +3,42 @@ import path from "node:path";
|
||||
|
||||
/**
|
||||
* What the packager knows about a package whose files were bundled: where it is, what it is called,
|
||||
* and whether it is third-party software. A package installed under a `node_modules` directory is
|
||||
* third-party; one that is not (a workspace package, or the module itself) is the platform's or the
|
||||
* module's own code.
|
||||
* and whose code it is.
|
||||
*
|
||||
* - **Ekklesia-owned** (`ekklesiaOwned`): it identifies itself as a published Ekklesia
|
||||
* module-development package (`isEkklesiaPackage`), wherever it was installed from: a workspace
|
||||
* link or a registry copy under `node_modules` (ADR 0053). It is recorded under `incorporates`,
|
||||
* never credited as third-party software.
|
||||
* - **Third-party** (`thirdParty`): anything else installed under a `node_modules` directory.
|
||||
* - Otherwise it is the module's own code, or a local package of the module's own repository.
|
||||
*/
|
||||
export interface BundledPackage {
|
||||
readonly root: string;
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly ekklesiaOwned: boolean;
|
||||
readonly thirdParty: boolean;
|
||||
readonly manifest: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/** The marker every published Ekklesia module-development package carries in its package.json. */
|
||||
export const EKKLESIA_PACKAGE_DISTRIBUTION = "module-development";
|
||||
|
||||
/**
|
||||
* Whether a package is an Ekklesia module-development package: its name is in the `@ekklesia` scope
|
||||
* **and** its package.json says so explicitly (`"ekklesia": { "distribution": "module-development" }`).
|
||||
* Neither a path nor a scope name alone is enough.
|
||||
*/
|
||||
export function isEkklesiaPackage(manifest: Readonly<Record<string, unknown>>): boolean {
|
||||
const ekklesia = manifest.ekklesia;
|
||||
return (
|
||||
typeof manifest.name === "string" &&
|
||||
manifest.name.startsWith("@ekklesia/") &&
|
||||
isRecord(ekklesia) &&
|
||||
ekklesia.distribution === EKKLESIA_PACKAGE_DISTRIBUTION
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -48,11 +72,13 @@ export class BundledPackageIndex {
|
||||
visited.push(directory);
|
||||
const manifest = await readManifest(directory);
|
||||
if (manifest && typeof manifest.name === "string" && typeof manifest.version === "string") {
|
||||
const ekklesiaOwned = isEkklesiaPackage(manifest);
|
||||
found = Object.freeze({
|
||||
ekklesiaOwned,
|
||||
manifest: Object.freeze(manifest),
|
||||
name: manifest.name,
|
||||
root: directory,
|
||||
thirdParty: directory.split(path.sep).includes("node_modules"),
|
||||
thirdParty: !ekklesiaOwned && directory.split(path.sep).includes("node_modules"),
|
||||
version: manifest.version,
|
||||
});
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user