Fix StartOS 0.4 TypeScript packaging to match SDK API

This commit is contained in:
MacPro
2026-04-09 15:10:44 -05:00
parent 68ec875ee7
commit 8298c083c7
3436 changed files with 867051 additions and 92 deletions
@@ -0,0 +1,36 @@
import { Effects } from '../../../base/lib/Effects';
import { Manifest, PackageId } from '../../../base/lib/osBindings';
export declare class GetServiceManifest<Mapped = Manifest> {
readonly effects: Effects;
readonly packageId: PackageId;
readonly map: (manifest: Manifest | null) => Mapped;
readonly eq: (a: Mapped, b: Mapped) => boolean;
constructor(effects: Effects, packageId: PackageId, map: (manifest: Manifest | null) => Mapped, eq: (a: Mapped, b: Mapped) => boolean);
/**
* Returns the manifest of a service. Reruns the context from which it has been called if the underlying value changes
*/
const(): Promise<Mapped>;
/**
* Returns the manifest of a service. Does nothing if it changes
*/
once(): Promise<Mapped>;
private watchGen;
/**
* Watches the manifest of a service. Returns an async iterator that yields whenever the value changes
*/
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown>;
/**
* Watches the manifest of a service. Takes a custom callback function to run whenever it changes
*/
onChange(callback: (value: Mapped | null, error?: Error) => {
cancel: boolean;
} | Promise<{
cancel: boolean;
}>): void;
/**
* Watches the manifest of a service. Returns when the predicate is true
*/
waitFor(pred: (value: Mapped) => boolean): Promise<Mapped>;
}
export declare function getServiceManifest(effects: Effects, packageId: PackageId): GetServiceManifest<Manifest>;
export declare function getServiceManifest<Mapped>(effects: Effects, packageId: PackageId, map: (manifest: Manifest | null) => Mapped, eq?: (a: Mapped, b: Mapped) => boolean): GetServiceManifest<Mapped>;
@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetServiceManifest = void 0;
exports.getServiceManifest = getServiceManifest;
const AbortedError_1 = require("../../../base/lib/util/AbortedError");
const Drop_1 = require("../../../base/lib/util/Drop");
const deepEqual_1 = require("../../../base/lib/util/deepEqual");
class GetServiceManifest {
constructor(effects, packageId, map, eq) {
this.effects = effects;
this.packageId = packageId;
this.map = map;
this.eq = eq;
}
/**
* Returns the manifest of a service. Reruns the context from which it has been called if the underlying value changes
*/
async const() {
let abort = new AbortController();
const watch = this.watch(abort.signal);
const res = await watch.next();
if (this.effects.constRetry) {
watch
.next()
.then(() => {
abort.abort();
this.effects.constRetry && this.effects.constRetry();
})
.catch();
}
return res.value;
}
/**
* Returns the manifest of a service. Does nothing if it changes
*/
async once() {
const manifest = await this.effects.getServiceManifest({
packageId: this.packageId,
});
return this.map(manifest);
}
async *watchGen(abort) {
let prev = null;
const resolveCell = { resolve: () => { } };
this.effects.onLeaveContext(() => {
resolveCell.resolve();
});
abort?.addEventListener('abort', () => resolveCell.resolve());
while (this.effects.isInContext && !abort?.aborted) {
let callback = () => { };
const waitForNext = new Promise((resolve) => {
callback = resolve;
resolveCell.resolve = resolve;
});
const next = this.map(await this.effects.getServiceManifest({
packageId: this.packageId,
callback: () => callback(),
}));
if (!prev || !this.eq(prev.value, next)) {
prev = { value: next };
yield next;
}
await waitForNext;
}
return new Promise((_, rej) => rej(new AbortedError_1.AbortedError()));
}
/**
* Watches the manifest of a service. Returns an async iterator that yields whenever the value changes
*/
watch(abort) {
const ctrl = new AbortController();
abort?.addEventListener('abort', () => ctrl.abort());
return Drop_1.DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort());
}
/**
* Watches the manifest of a service. Takes a custom callback function to run whenever it changes
*/
onChange(callback) {
;
(async () => {
const ctrl = new AbortController();
for await (const value of this.watch(ctrl.signal)) {
try {
const res = await callback(value);
if (res.cancel) {
ctrl.abort();
break;
}
}
catch (e) {
console.error('callback function threw an error @ GetServiceManifest.onChange', e);
}
}
})()
.catch((e) => callback(null, e))
.catch((e) => console.error('callback function threw an error @ GetServiceManifest.onChange', e));
}
/**
* Watches the manifest of a service. Returns when the predicate is true
*/
waitFor(pred) {
const ctrl = new AbortController();
return Drop_1.DropPromise.of(Promise.resolve().then(async () => {
for await (const next of this.watchGen(ctrl.signal)) {
if (pred(next)) {
return next;
}
}
throw new Error('context left before predicate passed');
}), () => ctrl.abort());
}
}
exports.GetServiceManifest = GetServiceManifest;
function getServiceManifest(effects, packageId, map, eq) {
return new GetServiceManifest(effects, packageId, map ?? ((a) => a), eq ?? ((a, b) => (0, deepEqual_1.deepEqual)(a, b)));
}
//# sourceMappingURL=GetServiceManifest.js.map
@@ -0,0 +1 @@
{"version":3,"file":"GetServiceManifest.js","sourceRoot":"","sources":["../../../../package/lib/util/GetServiceManifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
import { T } from '..';
import { Effects } from '../../../base/lib/Effects';
export declare class GetSslCertificate {
readonly effects: Effects;
readonly hostnames: string[];
readonly algorithm?: T.Algorithm | undefined;
constructor(effects: Effects, hostnames: string[], algorithm?: T.Algorithm | undefined);
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Restarts the service if it changes
*/
const(): Promise<[string, string, string]>;
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Does nothing if it changes
*/
once(): Promise<[string, string, string]>;
private watchGen;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns an async iterator that yields whenever the value changes
*/
watch(abort?: AbortSignal): AsyncGenerator<[string, string, string], never, unknown>;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Takes a custom callback function to run whenever it changes
*/
onChange(callback: (value: [string, string, string] | null, error?: Error) => {
cancel: boolean;
} | Promise<{
cancel: boolean;
}>): void;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns when the predicate is true
*/
waitFor(pred: (value: [string, string, string] | null) => boolean): Promise<[string, string, string] | null>;
}
@@ -0,0 +1,100 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetSslCertificate = void 0;
const AbortedError_1 = require("../../../base/lib/util/AbortedError");
const Drop_1 = require("../../../base/lib/util/Drop");
class GetSslCertificate {
constructor(effects, hostnames, algorithm) {
this.effects = effects;
this.hostnames = hostnames;
this.algorithm = algorithm;
}
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Restarts the service if it changes
*/
const() {
return this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
callback: this.effects.constRetry &&
(() => this.effects.constRetry && this.effects.constRetry()),
});
}
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Does nothing if it changes
*/
once() {
return this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
});
}
async *watchGen(abort) {
const resolveCell = { resolve: () => { } };
this.effects.onLeaveContext(() => {
resolveCell.resolve();
});
abort?.addEventListener('abort', () => resolveCell.resolve());
while (this.effects.isInContext && !abort?.aborted) {
let callback = () => { };
const waitForNext = new Promise((resolve) => {
callback = resolve;
resolveCell.resolve = resolve;
});
yield await this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
callback: () => callback(),
});
await waitForNext;
}
return new Promise((_, rej) => rej(new AbortedError_1.AbortedError()));
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns an async iterator that yields whenever the value changes
*/
watch(abort) {
const ctrl = new AbortController();
abort?.addEventListener('abort', () => ctrl.abort());
return Drop_1.DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort());
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Takes a custom callback function to run whenever it changes
*/
onChange(callback) {
;
(async () => {
const ctrl = new AbortController();
for await (const value of this.watch(ctrl.signal)) {
try {
const res = await callback(value);
if (res.cancel) {
ctrl.abort();
break;
}
}
catch (e) {
console.error('callback function threw an error @ GetSslCertificate.onChange', e);
}
}
})()
.catch((e) => callback(null, e))
.catch((e) => console.error('callback function threw an error @ GetSslCertificate.onChange', e));
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns when the predicate is true
*/
waitFor(pred) {
const ctrl = new AbortController();
return Drop_1.DropPromise.of(Promise.resolve().then(async () => {
for await (const next of this.watchGen(ctrl.signal)) {
if (pred(next)) {
return next;
}
}
return null;
}), () => ctrl.abort());
}
}
exports.GetSslCertificate = GetSslCertificate;
//# sourceMappingURL=GetSslCertificate.js.map
@@ -0,0 +1 @@
{"version":3,"file":"GetSslCertificate.js","sourceRoot":"","sources":["../../../../package/lib/util/GetSslCertificate.ts"],"names":[],"mappings":";;;AAEA,sEAAkE;AAClE,sDAAwE;AAExE,MAAa,iBAAiB;IAC5B,YACW,OAAgB,EAChB,SAAmB,EACnB,SAAuB;QAFvB,YAAO,GAAP,OAAO,CAAS;QAChB,cAAS,GAAT,SAAS,CAAU;QACnB,cAAS,GAAT,SAAS,CAAc;IAC/B,CAAC;IAEJ;;OAEG;IACH,KAAK;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;YACpC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EACN,IAAI,CAAC,OAAO,CAAC,UAAU;gBACvB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;SAC/D,CAAC,CAAA;IACJ,CAAC;IACD;;OAEG;IACH,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;YACpC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAA;IACJ,CAAC;IAEO,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAmB;QACzC,MAAM,WAAW,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAA;QACzC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE;YAC/B,WAAW,CAAC,OAAO,EAAE,CAAA;QACvB,CAAC,CAAC,CAAA;QACF,KAAK,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC7D,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;YACnD,IAAI,QAAQ,GAAe,GAAG,EAAE,GAAE,CAAC,CAAA;YACnC,MAAM,WAAW,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAChD,QAAQ,GAAG,OAAO,CAAA;gBAClB,WAAW,CAAC,OAAO,GAAG,OAAO,CAAA;YAC/B,CAAC,CAAC,CAAA;YACF,MAAM,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;gBACzC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,EAAE;aAC3B,CAAC,CAAA;YACF,MAAM,WAAW,CAAA;QACnB,CAAC;QACD,OAAO,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,2BAAY,EAAE,CAAC,CAAC,CAAA;IAChE,CAAC;IAED;;OAEG;IACH,KAAK,CACH,KAAmB;QAEnB,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAA;QAClC,KAAK,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;QACpD,OAAO,oBAAa,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;IACzE,CAAC;IAED;;OAEG;IACH,QAAQ,CACN,QAGuD;QAEvD,CAAC;QAAA,CAAC,KAAK,IAAI,EAAE;YACX,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAA;YAClC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClD,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAA;oBACjC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;wBACf,IAAI,CAAC,KAAK,EAAE,CAAA;wBACZ,MAAK;oBACP,CAAC;gBACH,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,OAAO,CAAC,KAAK,CACX,+DAA+D,EAC/D,CAAC,CACF,CAAA;gBACH,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE;aACD,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;aAC/B,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CACX,OAAO,CAAC,KAAK,CACX,+DAA+D,EAC/D,CAAC,CACF,CACF,CAAA;IACL,CAAC;IAED;;OAEG;IACH,OAAO,CACL,IAAyD;QAEzD,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAA;QAClC,OAAO,kBAAW,CAAC,EAAE,CACnB,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAChC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACpD,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACf,OAAO,IAAI,CAAA;gBACb,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC,CAAC,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CACnB,CAAA;IACH,CAAC;CACF;AApHD,8CAoHC"}
+340
View File
@@ -0,0 +1,340 @@
import * as fs from 'fs/promises';
import * as T from '../../../base/lib/types';
import * as cp from 'child_process';
import { Buffer } from 'node:buffer';
import { Drop } from '../../../base/lib/util/Drop';
import { Mounts } from '../mainFn/Mounts';
import { BackupEffects } from '../backup/Backups';
import { PathBase } from './Volume';
export declare const execFile: typeof cp.execFile.__promisify__;
export type ExecOptions = {
input?: string | Buffer;
};
/**
* Interface representing an isolated container environment for running service processes.
*
* Provides methods for executing commands, spawning processes, mounting filesystems,
* and writing files within the container's rootfs. Comes in two flavors:
* {@link SubContainerOwned} (owns the underlying filesystem) and
* {@link SubContainerRc} (reference-counted handle to a shared container).
*/
export interface SubContainer<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop, PathBase {
readonly imageId: keyof Manifest['images'] & T.ImageId;
readonly rootfs: string;
readonly guid: T.Guid;
/**
* Get the absolute path to a file or directory within this subcontainer's rootfs
* @param path Path relative to the rootfs
*/
subpath(path: string): string;
/**
* Apply filesystem mounts (volumes, assets, dependencies, backups) to this subcontainer.
* @param mounts - The Mounts configuration to apply
* @returns This subcontainer instance for chaining
*/
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
/** Destroy this subcontainer and clean up its filesystem */
destroy: () => Promise<null>;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* Launch a command as the init (PID 1) process of the subcontainer.
* Replaces the current leader process.
* @param command - The command and arguments to execute
* @param options - Optional environment, working directory, and user overrides
*/
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
/**
* Spawn a command inside the subcontainer as a non-init process.
* @param command - The command and arguments to execute
* @param options - Optional environment, working directory, user, and stdio overrides
*/
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
/**
* Create a reference-counted handle to this subcontainer.
* The underlying container is only destroyed when all handles are released.
*/
rc(): SubContainerRc<Manifest, Effects>;
/** Returns true if this is an owned subcontainer (not a reference-counted handle) */
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
/**
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
*/
export declare class SubContainerOwned<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop implements SubContainer<Manifest, Effects> {
readonly effects: Effects;
readonly imageId: keyof Manifest['images'] & T.ImageId;
readonly rootfs: string;
readonly guid: T.Guid;
private destroyed;
rcs: number;
private leader;
private leaderExited;
private waitProc;
private constructor();
static of<Manifest extends T.SDKManifest, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string): Promise<SubContainerOwned<Manifest, Effects>>;
static withTemp<Manifest extends T.SDKManifest, T, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string, fn: (subContainer: SubContainer<Manifest, Effects>) => Promise<T>): Promise<T>;
subpath(path: string): string;
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
private killLeader;
get destroy(): () => Promise<null>;
onDrop(): void;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
rc(): SubContainerRc<Manifest, Effects>;
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
/**
* A reference-counted handle to a {@link SubContainerOwned}.
*
* Multiple `SubContainerRc` instances can share one underlying subcontainer.
* The subcontainer is destroyed only when the last reference is released via `destroy()`.
*/
export declare class SubContainerRc<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop implements SubContainer<Manifest, Effects> {
private readonly subcontainer;
get imageId(): keyof Manifest["images"] & string;
get rootfs(): string;
get guid(): string;
subpath(path: string): string;
private destroyed;
private destroying;
constructor(subcontainer: SubContainerOwned<Manifest, Effects>);
static of<Manifest extends T.SDKManifest, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string): Promise<SubContainerRc<Manifest, Effects>>;
static withTemp<Manifest extends T.SDKManifest, T, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string, fn: (subContainer: SubContainer<Manifest, Effects>) => Promise<T>): Promise<T>;
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
get destroy(): () => Promise<null>;
onDrop(): void;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
rc(): SubContainerRc<Manifest, Effects>;
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
export type CommandOptions = {
/**
* Environment variables to set for this command
*/
env?: {
[variable in string]?: string;
};
/**
* the working directory to run this command in
*/
cwd?: string;
/**
* the user to run this command as
*/
user?: string;
};
export type StdioOptions = {
stdio?: cp.IOType;
};
/** UID/GID mapping for mount id-remapping (see kernel idmappings docs) */
export type IdMap = {
fromId: number;
toId: number;
range: number;
};
/** Union of all mount option types supported by the subcontainer runtime */
export type MountOptions = MountOptionsVolume | MountOptionsAssets | MountOptionsPointer | MountOptionsBackup;
/** Mount options for binding a service volume into a subcontainer */
export type MountOptionsVolume = {
type: 'volume';
volumeId: string;
subpath: string | null;
readonly: boolean;
filetype: 'file' | 'directory' | 'infer';
idmap: IdMap[];
};
/** Mount options for binding packaged static assets into a subcontainer */
export type MountOptionsAssets = {
type: 'assets';
subpath: string | null;
filetype: 'file' | 'directory' | 'infer';
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/** Mount options for binding a dependency package's volume into a subcontainer */
export type MountOptionsPointer = {
type: 'pointer';
packageId: string;
volumeId: string;
subpath: string | null;
readonly: boolean;
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/** Mount options for binding the backup directory into a subcontainer */
export type MountOptionsBackup = {
type: 'backup';
subpath: string | null;
filetype: 'file' | 'directory' | 'infer';
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/**
* Error thrown when a subcontainer command exits with a non-zero code or signal.
* Contains the full result including stdout, stderr, exit code, and exit signal.
*/
export declare class ExitError extends Error {
readonly command: string;
readonly result: {
exitCode: number | null;
exitSignal: T.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
};
constructor(command: string, result: {
exitCode: number | null;
exitSignal: T.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
});
}
+582
View File
@@ -0,0 +1,582 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExitError = exports.SubContainerRc = exports.SubContainerOwned = exports.execFile = void 0;
const fs = __importStar(require("fs/promises"));
const cp = __importStar(require("child_process"));
const util_1 = require("util");
const node_buffer_1 = require("node:buffer");
const once_1 = require("../../../base/lib/util/once");
const Drop_1 = require("../../../base/lib/util/Drop");
exports.execFile = (0, util_1.promisify)(cp.execFile);
const False = () => false;
const TIMES_TO_WAIT_FOR_PROC = 100;
async function prepBind(from, to, type) {
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null;
const toMeta = await fs.stat(to).catch((_) => null);
if (type === 'file' || (type === 'infer' && from && fromMeta?.isFile())) {
if (toMeta && toMeta.isDirectory())
await fs.rmdir(to, { recursive: false });
if (from && !fromMeta) {
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ''), { recursive: true });
await fs.writeFile(from, '');
}
if (!toMeta) {
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ''), { recursive: true });
await fs.writeFile(to, '');
}
}
else {
if (toMeta && toMeta.isFile() && !toMeta.size)
await fs.rm(to);
if (from && !fromMeta)
await fs.mkdir(from, { recursive: true });
if (!toMeta)
await fs.mkdir(to, { recursive: true });
}
}
async function bind(from, to, type, idmap) {
await prepBind(from, to, type);
const args = ['--bind'];
if (idmap.length) {
args.push(`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(' ')}`);
}
await (0, exports.execFile)('mount', [...args, from, to]);
}
/**
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
*/
class SubContainerOwned extends Drop_1.Drop {
constructor(effects, imageId, rootfs, guid) {
super();
this.effects = effects;
this.imageId = imageId;
this.rootfs = rootfs;
this.guid = guid;
this.destroyed = false;
this.rcs = 0;
this.leaderExited = false;
this.leaderExited = false;
this.leader = cp.spawn('start-container', ['subcontainer', 'launch', rootfs], {
killSignal: 'SIGKILL',
stdio: 'inherit',
});
this.leader.on('exit', () => {
this.leaderExited = true;
});
this.waitProc = (0, once_1.once)(() => new Promise(async (resolve, reject) => {
let count = 0;
while (!(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False))) {
if (count++ > TIMES_TO_WAIT_FOR_PROC) {
console.debug('Failed to start subcontainer', {
guid: this.guid,
imageId: this.imageId,
rootfs: this.rootfs,
});
return reject(new Error(`Failed to start subcontainer ${this.imageId}`));
}
await wait(1);
}
resolve(null);
}));
}
static async of(effects, image, mounts, name) {
const { imageId, sharedRun } = image;
const [rootfs, guid] = await effects.subcontainer.createFs({
imageId,
name,
});
const res = new SubContainerOwned(effects, imageId, rootfs, guid);
try {
if (mounts) {
await res.mount(mounts);
}
const shared = ['dev', 'sys'];
if (!!sharedRun) {
shared.push('run');
}
await fs.mkdir(`${rootfs}/etc`, { recursive: true });
await fs.copyFile('/etc/resolv.conf', `${rootfs}/etc/resolv.conf`);
for (const dirPart of shared) {
const from = `/${dirPart}`;
const to = `${rootfs}/${dirPart}`;
await fs.mkdir(from, { recursive: true });
await fs.mkdir(to, { recursive: true });
await (0, exports.execFile)('mount', ['--rbind', from, to]);
}
return res;
}
catch (e) {
await res.destroy();
throw e;
}
}
static async withTemp(effects, image, mounts, name, fn) {
const subContainer = await SubContainerOwned.of(effects, image, mounts, name);
try {
return await fn(subContainer);
}
finally {
await subContainer.destroy();
}
}
subpath(path) {
return path.startsWith('/')
? `${this.rootfs}${path}`
: `${this.rootfs}/${path}`;
}
async mount(mounts) {
for (let mount of mounts.build()) {
let { options, mountpoint } = mount;
const path = mountpoint.startsWith('/')
? `${this.rootfs}${mountpoint}`
: `${this.rootfs}/${mountpoint}`;
if (options.type === 'volume') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/volumes/${options.volumeId}${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else if (options.type === 'assets') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/assets/${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else if (options.type === 'pointer') {
await prepBind(null, path, 'directory');
await this.effects.mount({ location: path, target: options });
}
else if (options.type === 'backup') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/backup${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else {
throw new Error(`unknown type ${options.type}`);
}
}
return this;
}
async killLeader() {
if (this.leaderExited) {
return;
}
return new Promise((resolve, reject) => {
try {
let timeout = setTimeout(() => this.leader.kill('SIGKILL'), 30000);
this.leader.on('exit', () => {
clearTimeout(timeout);
resolve(null);
});
if (!this.leader.kill('SIGTERM')) {
reject(new Error('kill(2) failed'));
}
}
catch (e) {
reject(e);
}
});
}
get destroy() {
return async () => {
if (!this.destroyed) {
const guid = this.guid;
await this.killLeader();
await this.effects.subcontainer.destroyFs({ guid });
this.destroyed = true;
}
return null;
};
}
onDrop() {
console.log(`Cleaning up dangling subcontainer ${this.guid}`);
this.destroy();
}
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async exec(command, options, timeoutMs = 30000, abort) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options?.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`);
}
}
const child = cp.spawn('start-container', [
'subcontainer',
'exec',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], options || {});
abort?.signal.addEventListener('abort', () => child.kill('SIGKILL'));
if (options?.input) {
await new Promise((resolve, reject) => {
try {
child.stdin.on('error', (e) => reject(e));
child.stdin.write(options.input, (e) => {
if (e) {
reject(e);
}
else {
resolve(null);
}
});
}
catch (e) {
reject(e);
}
});
await new Promise((resolve, reject) => {
try {
child.stdin.end(resolve);
}
catch (e) {
reject(e);
}
});
}
const stdout = { data: '' };
const stderr = { data: '' };
const appendData = (appendTo) => (chunk) => {
if (typeof chunk === 'string' || chunk instanceof node_buffer_1.Buffer) {
appendTo.data += chunk.toString();
}
else {
console.error('received unexpected chunk', chunk);
}
};
return new Promise((resolve, reject) => {
child.on('error', reject);
let killTimeout;
if (timeoutMs !== null && child.pid) {
killTimeout = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
}
child.stdout.on('data', appendData(stdout));
child.stderr.on('data', appendData(stderr));
child.on('exit', (code, signal) => {
clearTimeout(killTimeout);
const result = {
exitCode: code,
exitSignal: signal,
stdout: stdout.data,
stderr: stderr.data,
};
resolve({
throw: () => !code && !signal
? { stdout: stdout.data, stderr: stderr.data }
: (() => {
throw new ExitError(command[0], result);
})(),
...result,
});
});
});
}
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async execFail(command, options, timeoutMs, abort) {
return this.exec(command, options, timeoutMs, abort).then((res) => res.throw());
}
async launch(command, options) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options?.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env).filter(([_, v]) => v != undefined)) {
extra.push(`--env=${k}=${v}`);
}
}
await this.killLeader();
this.leaderExited = false;
this.leader = cp.spawn('start-container', [
'subcontainer',
'launch',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], { ...options, stdio: 'inherit' });
this.leader.on('exit', () => {
this.leaderExited = true;
});
return this.leader;
}
async spawn(command, options = { stdio: 'inherit' }) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env).filter(([_, v]) => v != undefined)) {
extra.push(`--env=${k}=${v}`);
}
}
return cp.spawn('start-container', [
'subcontainer',
'exec',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], options);
}
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
async writeFile(path, data, options) {
const fullPath = this.subpath(path);
const dir = fullPath.replace(/\/[^/]*\/?$/, '');
await fs.mkdir(dir, { recursive: true });
return fs.writeFile(fullPath, data, options);
}
rc() {
return new SubContainerRc(this);
}
isOwned() {
return true;
}
}
exports.SubContainerOwned = SubContainerOwned;
/**
* A reference-counted handle to a {@link SubContainerOwned}.
*
* Multiple `SubContainerRc` instances can share one underlying subcontainer.
* The subcontainer is destroyed only when the last reference is released via `destroy()`.
*/
class SubContainerRc extends Drop_1.Drop {
get imageId() {
return this.subcontainer.imageId;
}
get rootfs() {
return this.subcontainer.rootfs;
}
get guid() {
return this.subcontainer.guid;
}
subpath(path) {
return this.subcontainer.subpath(path);
}
constructor(subcontainer) {
subcontainer.rcs++;
super();
this.subcontainer = subcontainer;
this.destroyed = false;
this.destroying = null;
}
static async of(effects, image, mounts, name) {
return new SubContainerRc(await SubContainerOwned.of(effects, image, mounts, name));
}
static async withTemp(effects, image, mounts, name, fn) {
const subContainer = await SubContainerRc.of(effects, image, mounts, name);
try {
return await fn(subContainer);
}
finally {
await subContainer.destroy();
}
}
async mount(mounts) {
await this.subcontainer.mount(mounts);
return this;
}
get destroy() {
return async () => {
if (!this.destroyed && !this.destroying) {
const rcs = --this.subcontainer.rcs;
if (rcs <= 0) {
this.destroying = this.subcontainer.destroy();
if (rcs < 0)
console.error(new Error('UNREACHABLE: rcs < 0').stack);
}
}
if (this.destroying) {
await this.destroying;
}
this.destroyed = true;
this.destroying = null;
return null;
};
}
onDrop() {
this.destroy();
}
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async exec(command, options, timeoutMs, abort) {
return this.subcontainer.exec(command, options, timeoutMs, abort);
}
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async execFail(command, options, timeoutMs, abort) {
return this.subcontainer.execFail(command, options, timeoutMs, abort);
}
async launch(command, options) {
return this.subcontainer.launch(command, options);
}
async spawn(command, options = { stdio: 'inherit' }) {
return this.subcontainer.spawn(command, options);
}
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
async writeFile(path, data, options) {
return this.subcontainer.writeFile(path, data, options);
}
rc() {
return this.subcontainer.rc();
}
isOwned() {
return false;
}
}
exports.SubContainerRc = SubContainerRc;
function wait(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
/**
* Error thrown when a subcontainer command exits with a non-zero code or signal.
* Contains the full result including stdout, stderr, exit code, and exit signal.
*/
class ExitError extends Error {
constructor(command, result) {
let message;
if (result.exitCode) {
message = `${command} failed with exit code ${result.exitCode}: ${result.stderr}`;
}
else if (result.exitSignal) {
message = `${command} terminated with signal ${result.exitSignal}: ${result.stderr}`;
}
else {
message = `${command} succeeded: ${result.stdout}`;
}
super(message);
this.command = command;
this.result = result;
}
}
exports.ExitError = ExitError;
//# sourceMappingURL=SubContainer.js.map
File diff suppressed because one or more lines are too long
+48
View File
@@ -0,0 +1,48 @@
import * as fs from 'node:fs/promises';
import * as T from '../../../base/lib/types';
/**
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
*/
export interface PathBase {
subpath(path: string): string;
}
/**
* @description Represents a volume in the StartOS filesystem.
* Provides utilities for reading and writing files within the volume.
*/
export declare class Volume<Id extends string = string> implements PathBase {
readonly id: Id;
/**
* The absolute path to this volume's root directory
*/
readonly path: string;
constructor(id: Id);
/**
* Get the absolute path to a file or directory within this volume
* @param subpath Path relative to the volume root
*/
subpath(subpath: string): string;
/**
* @description Read a file from this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param options Optional read options (same as node:fs/promises readFile)
*/
readFile(subpath: string, options?: Parameters<typeof fs.readFile>[1]): Promise<Buffer | string>;
/**
* @description Write a file to this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(subpath: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
}
/**
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
*/
export type Volumes<Manifest extends T.SDKManifest> = {
[K in Manifest['volumes'][number]]: Volume<K>;
};
/**
* Creates a type-safe volumes object from a manifest
*/
export declare function createVolumes<Manifest extends T.SDKManifest>(manifest: Manifest): Volumes<Manifest>;
+91
View File
@@ -0,0 +1,91 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Volume = void 0;
exports.createVolumes = createVolumes;
const fs = __importStar(require("node:fs/promises"));
/**
* @description Represents a volume in the StartOS filesystem.
* Provides utilities for reading and writing files within the volume.
*/
class Volume {
constructor(id) {
this.id = id;
this.path = `/media/startos/volumes/${id}`;
}
/**
* Get the absolute path to a file or directory within this volume
* @param subpath Path relative to the volume root
*/
subpath(subpath) {
return subpath.startsWith('/')
? `${this.path}${subpath}`
: `${this.path}/${subpath}`;
}
/**
* @description Read a file from this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param options Optional read options (same as node:fs/promises readFile)
*/
async readFile(subpath, options) {
const fullPath = this.subpath(subpath);
return fs.readFile(fullPath, options);
}
/**
* @description Write a file to this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
async writeFile(subpath, data, options) {
const fullPath = this.subpath(subpath);
const dir = fullPath.replace(/\/[^/]*\/?$/, '');
await fs.mkdir(dir, { recursive: true });
return fs.writeFile(fullPath, data, options);
}
}
exports.Volume = Volume;
/**
* Creates a type-safe volumes object from a manifest
*/
function createVolumes(manifest) {
const volumes = {};
for (const volumeId of manifest.volumes) {
;
volumes[volumeId] = new Volume(volumeId);
}
return volumes;
}
//# sourceMappingURL=Volume.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Volume.js","sourceRoot":"","sources":["../../../../package/lib/util/Volume.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EA,sCAQC;AAvFD,qDAAsC;AAUtC;;;GAGG;AACH,MAAa,MAAM;IAMjB,YAAqB,EAAM;QAAN,OAAE,GAAF,EAAE,CAAI;QACzB,IAAI,CAAC,IAAI,GAAG,0BAA0B,EAAE,EAAE,CAAA;IAC5C,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,OAAe;QACrB,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAC5B,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE;YAC1B,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,EAAE,CAAA;IAC/B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CACZ,OAAe,EACf,OAA2C;QAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,CACb,OAAe,EACf,IAIkD,EAClD,OAA4C;QAE5C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;QAC/C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,OAAO,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;CACF;AArDD,wBAqDC;AASD;;GAEG;AACH,SAAgB,aAAa,CAC3B,QAAkB;IAElB,MAAM,OAAO,GAAG,EAAuB,CAAA;IACvC,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACxC,CAAC;QAAC,OAAe,CAAC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
+182
View File
@@ -0,0 +1,182 @@
import { type X2jOptions, type XmlBuilderOptions } from 'fast-xml-parser';
import * as INI from 'ini';
import { z } from 'zod';
import * as T from '../../../base/lib/types';
import { PathBase } from './Volume';
/**
* Bidirectional transformers for converting between the raw file format and
* the application-level data type. Used with FileHelper factory methods.
*
* @typeParam Raw - The native type the file format parses to (e.g. `Record<string, unknown>` for JSON)
* @typeParam Transformed - The application-level type after transformation
*/
export type Transformers<Raw = unknown, Transformed = unknown, Validated extends Transformed = Transformed> = {
/** Transform raw parsed data into the application type */
onRead: (value: Raw) => Transformed;
/** Transform application data back into the raw format for writing */
onWrite: (value: Validated) => Raw;
};
type ToPath = string | {
base: PathBase;
subpath: string;
};
type Validator<_T, U> = z.ZodType<U>;
type ReadType<A> = {
once: () => Promise<A | null>;
const: (effects: T.Effects) => Promise<A | null>;
watch: (effects: T.Effects, abort?: AbortSignal) => AsyncGenerator<A | null, never, unknown>;
onChange: (effects: T.Effects, callback: (value: A | null, error?: Error) => {
cancel: boolean;
} | Promise<{
cancel: boolean;
}>) => void;
waitFor: (effects: T.Effects, pred: (value: A | null) => boolean) => Promise<A | null>;
};
/**
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
*
* These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
*
* It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
*
* Officially supported file types are json, yaml, and toml. Other files types can use "raw"
*
* Choose between officially supported file formats (), or a custom format (raw).
*
* @example
* Below are a few examples
*
* ```
* import { matches, FileHelper } from '@start9labs/start-sdk'
* const { arrayOf, boolean, literal, literals, object, natural, string } = matches
*
* export const jsonFile = FileHelper.json('./inputSpec.json', object({
* passwords: arrayOf(string).onMismatch([])
* type: literals('private', 'public').optional().onMismatch(undefined)
* }))
*
* export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
* url: literal('https://start9.com').onMismatch('https://start9.com')
* public: boolean.onMismatch(true)
* }))
*
* export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
* name: string.optional().onMismatch(undefined)
* age: natural.optional().onMismatch(undefined)
* }))
*
* export const bitcoinConfFile = FileHelper.raw(
* './service.conf',
* (obj: CustomType) => customConvertObjToFormattedString(obj),
* (str) => customParseStringToTypedObj(str),
* )
* ```
*/
export declare class FileHelper<A> {
readonly path: string;
readonly writeData: (dataIn: A) => string;
readonly readData: (stringValue: string) => unknown;
readonly validate: (value: unknown) => A;
private consts;
protected constructor(path: string, writeData: (dataIn: A) => string, readData: (stringValue: string) => unknown, validate: (value: unknown) => A);
private writeFileRaw;
/**
* Accepts structured data and overwrites the existing file on disk.
*/
private writeFile;
private readFileRaw;
private readFile;
/**
* Reads the file from disk and converts it to structured data.
*/
private readOnce;
private createFileWatchable;
/**
* Create a reactive reader for this file.
*
* Returns an object with multiple read strategies:
* - `once()` - Read the file once and return the parsed value
* - `const(effects)` - Read once but re-read when the file changes (for use with constRetry)
* - `watch(effects)` - Async generator yielding new values on each file change
* - `onChange(effects, callback)` - Fire a callback on each file change
* - `waitFor(effects, predicate)` - Block until the file value satisfies a predicate
*
* @param map - Optional transform function applied after validation
* @param eq - Optional equality function to deduplicate watch emissions
*/
read(): ReadType<A>;
read<B>(map: (value: A) => B, eq?: (left: B | null, right: B | null) => boolean): ReadType<B>;
/**
* Accepts full structured data and overwrites the existing file on disk if it exists.
*/
write(effects: T.Effects, data: T.AllowReadonly<A> | A, options?: {
allowWriteAfterConst?: boolean;
}): Promise<null>;
/**
* Accepts partial structured data and performs a merge with the existing file on disk.
*/
merge(effects: T.Effects, data: T.AllowReadonly<T.DeepPartial<A>>, options?: {
allowWriteAfterConst?: boolean;
}): Promise<null>;
/**
* We wanted to be able to have a fileHelper, and just modify the path later in time.
* Like one behavior of another dependency or something similar.
*/
withPath(path: ToPath): FileHelper<A>;
/**
* Create a File Helper for an arbitrary file type.
*
* Provide custom functions for translating data to/from the file format.
*/
static raw<A>(path: ToPath, toFile: (dataIn: A) => string, fromFile: (rawData: string) => unknown, validate: (data: unknown) => A): FileHelper<A>;
private static rawTransformed;
/**
* Create a File Helper for a text file
*/
static string(path: ToPath): FileHelper<string>;
static string<A extends string>(path: ToPath, shape: Validator<string, A>): FileHelper<A>;
static string<A extends Transformed, Transformed = string>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<string, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for a .json file.
*/
static json<A>(path: ToPath, shape: Validator<unknown, A>): FileHelper<A>;
static json<A extends Transformed, Transformed = unknown>(path: ToPath, shape: Validator<unknown, A>, transformers: Transformers<unknown, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for a .yaml file
*/
static yaml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>): FileHelper<A>;
static yaml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for a .toml file
*/
static toml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>): FileHelper<A>;
static toml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for a .ini file.
*
* Supports optional encode/decode options and custom transformers.
*/
static ini<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>, options?: INI.EncodeOptions & INI.DecodeOptions): FileHelper<A>;
static ini<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, options: INI.EncodeOptions & INI.DecodeOptions, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for a .env file (KEY=VALUE format, one per line).
*
* Lines starting with `#` are treated as comments and ignored on read.
*/
static env<A extends Record<string, string>>(path: ToPath, shape: Validator<Record<string, string>, A>): FileHelper<A>;
static env<A extends Transformed, Transformed = Record<string, string>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, string>, Transformed, A>): FileHelper<A>;
/**
* Create a File Helper for an .xml file.
*
* Supports optional parser/builder options from `fast-xml-parser`.
*/
static xml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>, options?: {
parser?: X2jOptions;
builder?: XmlBuilderOptions;
}): FileHelper<A>;
static xml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, options: {
parser?: X2jOptions;
builder?: XmlBuilderOptions;
}, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
}
export default FileHelper;
+380
View File
@@ -0,0 +1,380 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileHelper = void 0;
const TOML = __importStar(require("@iarna/toml"));
const fast_xml_parser_1 = require("fast-xml-parser");
const INI = __importStar(require("ini"));
const fs = __importStar(require("node:fs/promises"));
const YAML = __importStar(require("yaml"));
const zod_1 = require("zod");
const util_1 = require("../../../base/lib/util");
const Watchable_1 = require("../../../base/lib/util/Watchable");
const previousPath = /(.+?)\/([^/]*)$/;
const exists = (path) => fs.access(path).then(() => true, () => false);
async function onCreated(path) {
if (path === '/')
return;
if (!path.startsWith('/'))
path = `${process.cwd()}/${path}`;
if (await exists(path)) {
return;
}
const split = path.split('/');
const filename = split.pop();
const parent = split.join('/');
await onCreated(parent);
const ctrl = new AbortController();
const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal });
if (await exists(path)) {
ctrl.abort();
return;
}
if (await fs.access(path).then(() => true, () => false)) {
ctrl.abort();
return;
}
for await (let event of watch) {
if (event.filename === filename) {
ctrl.abort('finished');
return;
}
}
}
function fileMerge(...args) {
let res = args.shift();
for (const arg of args) {
if (res === arg)
continue;
else if (res &&
arg &&
typeof res === 'object' &&
typeof arg === 'object' &&
!Array.isArray(res) &&
!Array.isArray(arg)) {
for (const key of Object.keys(arg)) {
res[key] = fileMerge(res[key], arg[key]);
}
}
else
res = arg;
}
return res;
}
function filterUndefined(a) {
if (a && typeof a === 'object') {
if (Array.isArray(a)) {
return a.map(filterUndefined);
}
return Object.entries(a).reduce((acc, [k, v]) => {
if (v !== undefined) {
acc[k] = filterUndefined(v);
}
return acc;
}, {});
}
return a;
}
function toPath(path) {
if (typeof path === 'string') {
return path;
}
return path.base.subpath(path.subpath);
}
/**
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
*
* These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
*
* It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
*
* Officially supported file types are json, yaml, and toml. Other files types can use "raw"
*
* Choose between officially supported file formats (), or a custom format (raw).
*
* @example
* Below are a few examples
*
* ```
* import { matches, FileHelper } from '@start9labs/start-sdk'
* const { arrayOf, boolean, literal, literals, object, natural, string } = matches
*
* export const jsonFile = FileHelper.json('./inputSpec.json', object({
* passwords: arrayOf(string).onMismatch([])
* type: literals('private', 'public').optional().onMismatch(undefined)
* }))
*
* export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
* url: literal('https://start9.com').onMismatch('https://start9.com')
* public: boolean.onMismatch(true)
* }))
*
* export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
* name: string.optional().onMismatch(undefined)
* age: natural.optional().onMismatch(undefined)
* }))
*
* export const bitcoinConfFile = FileHelper.raw(
* './service.conf',
* (obj: CustomType) => customConvertObjToFormattedString(obj),
* (str) => customParseStringToTypedObj(str),
* )
* ```
*/
class FileHelper {
constructor(path, writeData, readData, validate) {
this.path = path;
this.writeData = writeData;
this.readData = readData;
this.validate = validate;
this.consts = [];
}
async writeFileRaw(data) {
const parent = previousPath.exec(this.path);
if (parent) {
await fs.mkdir(parent[1], { recursive: true });
}
await fs.writeFile(this.path, data);
return null;
}
/**
* Accepts structured data and overwrites the existing file on disk.
*/
async writeFile(data) {
return await this.writeFileRaw(this.writeData(data));
}
async readFileRaw() {
if (!(await exists(this.path))) {
return null;
}
return await fs.readFile(this.path).then((data) => data.toString('utf-8'));
}
async readFile() {
const raw = await this.readFileRaw();
if (raw === null) {
return raw;
}
return this.readData(raw);
}
/**
* Reads the file from disk and converts it to structured data.
*/
async readOnce(map) {
const data = await this.readFile();
if (!data)
return null;
return map(this.validate(data));
}
createFileWatchable(effects, map, eq) {
const doRead = async () => {
const data = await this.readFile();
if (!data)
return null;
return this.validate(data);
};
const filePath = this.path;
const fileHelper = this;
const wrappedMap = (raw) => {
if (raw === null)
return null;
return map(raw);
};
return new (class extends Watchable_1.Watchable {
constructor() {
super(...arguments);
this.label = 'FileHelper';
}
async fetch() {
return doRead();
}
async *produce(abort) {
while (this.effects.isInContext && !abort.aborted) {
if (await exists(filePath)) {
const ctrl = new AbortController();
abort.addEventListener('abort', () => ctrl.abort());
const watch = fs.watch(filePath, {
persistent: false,
signal: ctrl.signal,
});
yield await doRead();
await Promise.resolve()
.then(async () => {
for await (const _ of watch) {
ctrl.abort();
return null;
}
})
.catch((e) => console.error((0, util_1.asError)(e)));
}
else {
yield null;
await onCreated(filePath).catch((e) => console.error((0, util_1.asError)(e)));
}
}
}
onConstRegistered(value) {
if (!this.effects.constRetry)
return;
const record = [
this.effects.constRetry,
value,
wrappedMap,
eq,
];
fileHelper.consts.push(record);
return () => {
fileHelper.consts = fileHelper.consts.filter((r) => r !== record);
};
}
})(effects, { map: wrappedMap, eq });
}
read(map, eq) {
map = map ?? ((a) => a);
eq = eq ?? util_1.deepEqual;
return {
once: () => this.readOnce(map),
const: (effects) => this.createFileWatchable(effects, map, eq).const(),
watch: (effects, abort) => this.createFileWatchable(effects, map, eq).watch(abort),
onChange: (effects, callback) => this.createFileWatchable(effects, map, eq).onChange(callback),
waitFor: (effects, pred) => this.createFileWatchable(effects, map, eq).waitFor(pred),
};
}
/**
* Accepts full structured data and overwrites the existing file on disk if it exists.
*/
async write(effects, data, options = {}) {
const newData = this.validate(data);
await this.writeFile(newData);
if (!options.allowWriteAfterConst && effects.constRetry) {
const records = this.consts.filter(([c]) => c === effects.constRetry);
for (const record of records) {
const [_, prev, map, eq] = record;
if (!eq(prev, map(newData))) {
throw new Error(`Canceled: write after const: ${this.path}`);
}
}
}
return null;
}
/**
* Accepts partial structured data and performs a merge with the existing file on disk.
*/
async merge(effects, data, options = {}) {
const fileDataRaw = await this.readFileRaw();
let fileData = fileDataRaw === null ? null : this.readData(fileDataRaw);
try {
fileData = this.validate(fileData);
}
catch (_) { }
const mergeData = this.validate(fileMerge({}, fileData, data));
const toWrite = this.writeData(mergeData);
if (toWrite !== fileDataRaw) {
await this.writeFile(mergeData);
if (!options.allowWriteAfterConst && effects.constRetry) {
const records = this.consts.filter(([c]) => c === effects.constRetry);
for (const record of records) {
const [_, prev, map, eq] = record;
if (!eq(prev, map(mergeData))) {
throw new Error(`Canceled: write after const: ${this.path}`);
}
}
}
}
return null;
}
/**
* We wanted to be able to have a fileHelper, and just modify the path later in time.
* Like one behavior of another dependency or something similar.
*/
withPath(path) {
return new FileHelper(toPath(path), this.writeData, this.readData, this.validate);
}
/**
* Create a File Helper for an arbitrary file type.
*
* Provide custom functions for translating data to/from the file format.
*/
static raw(path, toFile, fromFile, validate) {
return new FileHelper(toPath(path), toFile, fromFile, validate);
}
static rawTransformed(path, toFile, fromFile, validate, transformers) {
return FileHelper.raw(path, (inData) => {
if (transformers) {
return toFile(transformers.onWrite(inData));
}
return toFile(inData);
}, (fileData) => {
if (transformers) {
return transformers.onRead(fromFile(fileData));
}
return fromFile(fileData);
}, validate);
}
static string(path, shape, transformers) {
return FileHelper.rawTransformed(path, (inData) => inData, (inString) => inString, (data) => (shape || zod_1.z.string()).parse(data), transformers);
}
static json(path, shape, transformers) {
return FileHelper.rawTransformed(path, (inData) => JSON.stringify(inData, null, 2), (inString) => JSON.parse(inString), (data) => shape.parse(data), transformers);
}
static yaml(path, shape, transformers) {
return FileHelper.rawTransformed(path, (inData) => YAML.stringify(inData, null, 2), (inString) => YAML.parse(inString), (data) => shape.parse(data), transformers);
}
static toml(path, shape, transformers) {
return FileHelper.rawTransformed(path, (inData) => TOML.stringify(inData), (inString) => TOML.parse(inString), (data) => shape.parse(data), transformers);
}
static ini(path, shape, options, transformers) {
return FileHelper.rawTransformed(path, (inData) => INI.stringify(filterUndefined(inData), options), (inString) => INI.parse(inString, options), (data) => shape.parse(data), transformers);
}
static env(path, shape, transformers) {
return FileHelper.rawTransformed(path, (inData) => Object.entries(inData)
.map(([k, v]) => `${k}=${v}`)
.join('\n'), (inString) => Object.fromEntries(inString
.split('\n')
.map((line) => line.trim())
.filter((line) => !line.startsWith('#') && line.includes('='))
.map((line) => {
const pos = line.indexOf('=');
return [line.slice(0, pos), line.slice(pos + 1)];
})), (data) => shape.parse(data), transformers);
}
static xml(path, shape, options, transformers) {
const parser = new fast_xml_parser_1.XMLParser(options?.parser);
const builder = new fast_xml_parser_1.XMLBuilder(options?.builder);
return FileHelper.rawTransformed(path, (inData) => builder.build(inData), (inString) => parser.parse(inString), (data) => shape.parse(data), transformers);
}
}
exports.FileHelper = FileHelper;
exports.default = FileHelper;
//# sourceMappingURL=fileHelper.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
export * from '../../../base/lib/util';
export { Drop } from '../../../base/lib/util/Drop';
export { Volume, Volumes } from './Volume';
+23
View File
@@ -0,0 +1,23 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Volume = exports.Drop = void 0;
__exportStar(require("../../../base/lib/util"), exports);
var Drop_1 = require("../../../base/lib/util/Drop");
Object.defineProperty(exports, "Drop", { enumerable: true, get: function () { return Drop_1.Drop; } });
var Volume_1 = require("./Volume");
Object.defineProperty(exports, "Volume", { enumerable: true, get: function () { return Volume_1.Volume; } });
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/util/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,yDAAsC;AAEtC,oDAAkD;AAAzC,4FAAA,IAAI,OAAA;AACb,mCAA0C;AAAjC,gGAAA,MAAM,OAAA"}