diff --git a/Makefile b/Makefile index 7fdb96a..d9ed9c1 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,17 @@ version = $(shell git tag --sort=committerdate | tail -1) test: $(TS_FILES) lib/test/output.ts npm test -make clean: - rm -rf dist +clean: + rm -rf dist/* | true lib/test/output.ts: lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts npm run buildOutput -bundle: fmt $(TS_FILES) package.json .FORCE node_modules test +bundle: clean fmt $(TS_FILES) package.json .FORCE node_modules test npx tsc + cp package.json dist/package.json + cp README.md dist/README.md + cp LICENSE dist/LICENSE check: npm run check @@ -22,9 +25,6 @@ node_modules: package.json npm install publish: clean bundle package.json README.md LICENSE - cp package.json dist/package.json - cp README.md dist/README.md - cp LICENSE dist/LICENSE cd dist && npm publish link: bundle cp package.json dist/package.json diff --git a/lib/actions/setupActions.ts b/lib/actions/setupActions.ts index 3ee7eb1..d31b665 100644 --- a/lib/actions/setupActions.ts +++ b/lib/actions/setupActions.ts @@ -8,13 +8,13 @@ export function setupActions(...createdActions: CreatedAction[]) { actions[action.metaData.id] = action.exportedAction; } - const manifestActions = async (effects: Effects) => { + const initializeActions = async (effects: Effects) => { for (const action of createdActions) { action.exportAction(effects); } }; return { actions, - manifestActions, + initializeActions, }; } diff --git a/lib/backup/Backups.ts b/lib/backup/Backups.ts index 0dd9022..b5e84e1 100644 --- a/lib/backup/Backups.ts +++ b/lib/backup/Backups.ts @@ -1,17 +1,18 @@ import { GenericManifest } from "../manifest/ManifestTypes"; import * as T from "../types"; +export type BACKUP = "BACKUP"; export const DEFAULT_OPTIONS: T.BackupOptions = { delete: true, force: true, ignoreExisting: false, exclude: [], }; -type BackupSet = { +type BackupSet = { srcPath: string; - srcVolume: string; + srcVolume: Volumes | BACKUP; dstPath: string; - dstVolume: string; + dstVolume: Volumes | BACKUP; options?: Partial; }; /** @@ -37,13 +38,13 @@ type BackupSet = { * ``` */ export class Backups { - static BACKUP = "BACKUP" as const; + static BACKUP: BACKUP = "BACKUP"; constructor( private options = DEFAULT_OPTIONS, - private backupSet = [] as BackupSet[], + private backupSet = [] as BackupSet[], ) {} - static volumes( + static volumes( ...volumeNames: Array ) { return new Backups().addSets( @@ -55,10 +56,14 @@ export class Backups { })), ); } - static addSets(...options: BackupSet[]) { + static addSets( + ...options: BackupSet[] + ) { return new Backups().addSets(...options); } - static with_options(options?: Partial) { + static with_options( + options?: Partial, + ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }); } set_options(options?: Partial) { @@ -78,7 +83,7 @@ export class Backups { })), ); } - addSets(...options: BackupSet[]) { + addSets(...options: BackupSet[]) { options.forEach((x) => this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }), ); diff --git a/lib/backup/setupBackups.ts b/lib/backup/setupBackups.ts index ed2f4f0..0327028 100644 --- a/lib/backup/setupBackups.ts +++ b/lib/backup/setupBackups.ts @@ -1,7 +1,7 @@ import { string } from "ts-matches"; import { Backups } from "."; import { GenericManifest } from "../manifest/ManifestTypes"; -import { BackupOptions } from "../types"; +import { BackupOptions, ExpectedExports } from "../types"; export type SetupBackupsParams = | [Partial, ...Array] diff --git a/lib/config/setupConfig.ts b/lib/config/setupConfig.ts index e4075d2..5fa841b 100644 --- a/lib/config/setupConfig.ts +++ b/lib/config/setupConfig.ts @@ -1,7 +1,7 @@ import { Config } from "./builder"; import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types"; import { InputSpec } from "./configTypes"; -import { Utils, nullIfEmpty, utils } from "../util"; +import { Utils, nullIfEmpty, once, utils } from "../util"; import { TypeFromProps } from "../util/propertiesMatcher"; declare const dependencyProof: unique symbol; @@ -30,11 +30,11 @@ export function setupConfig>( write: Save>, read: Read>, ) { - const validator = spec.validator(); + const validator = once(() => spec.validator()); return { setConfig: (async ({ effects, input }) => { - if (!validator.test(input)) { - await effects.error(String(validator.errorMessage(input))); + if (!validator().test(input)) { + await effects.error(String(validator().errorMessage(input))); return { error: "Set config type error for config" }; } await write({ diff --git a/lib/emverLite/emverList.test.ts b/lib/emverLite/emverList.test.ts index 7621850..7509873 100644 --- a/lib/emverLite/emverList.test.ts +++ b/lib/emverLite/emverList.test.ts @@ -9,7 +9,9 @@ describe("EmVer", () => { expect(checker.check("1.2.3.4")).toEqual(true); }); test("rangeOf('*') invalid", () => { + // @ts-expect-error expect(() => checker.check("a")).toThrow(); + // @ts-expect-error expect(() => checker.check("")).toThrow(); expect(() => checker.check("1..3")).toThrow(); }); @@ -18,6 +20,7 @@ describe("EmVer", () => { { const checker = rangeOf(">1.2.3.4"); test(`rangeOf(">1.2.3.4") valid`, () => { + expect(checker.check("2-beta123")).toEqual(true); expect(checker.check("2")).toEqual(true); expect(checker.check("1.2.3.5")).toEqual(true); expect(checker.check("1.2.3.4.1")).toEqual(true); diff --git a/lib/emverLite/mod.ts b/lib/emverLite/mod.ts index 5f66c44..cb7cde9 100644 --- a/lib/emverLite/mod.ts +++ b/lib/emverLite/mod.ts @@ -1,6 +1,8 @@ import * as matches from "ts-matches"; const starSub = /((\d+\.)*\d+)\.\*/; +// prettier-ignore +export type ValidEmVer = `${'>' | '<' | '>=' | '<=' | '=' | ''}${number | '*'}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`-${string}` | ""}`; function incrementLastNumber(list: number[]) { const newList = [...list]; @@ -61,7 +63,7 @@ export class EmVer { * Or an already made emver * IsUnsafe */ - static from(range: string | EmVer): EmVer { + static from(range: ValidEmVer | EmVer): EmVer { if (range instanceof EmVer) { return range; } @@ -71,22 +73,26 @@ export class EmVer { * Convert the range, should be 1.2.* or * into a emver * IsUnsafe */ - static parse(range: string): EmVer { + static parse(rangeExtra: string): EmVer { + const [range, extra] = rangeExtra.split("-"); const values = range.split(".").map((x) => parseInt(x)); for (const value of values) { if (isNaN(value)) { throw new Error(`Couldn't parse range: ${range}`); } } - return new EmVer(values); + return new EmVer(values, extra); } - private constructor(public readonly values: number[]) {} + private constructor( + public readonly values: number[], + readonly extra: string | null, + ) {} /** * Used when we need a new emver that has the last number incremented, used in the 1.* like things */ public withLastIncremented() { - return new EmVer(incrementLastNumber(this.values)); + return new EmVer(incrementLastNumber(this.values), null); } public greaterThan(other: EmVer): boolean { @@ -153,6 +159,10 @@ export class EmVer { .when("less", () => -1 as const) .unwrap(); } + + toString() { + return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}`; + } } /** @@ -248,7 +258,7 @@ export class Checker { * Check is the function that will be given a emver or unparsed emver and should give if it follows * a pattern */ - public readonly check: (value: string | EmVer) => boolean, + public readonly check: (value: ValidEmVer | EmVer) => boolean, ) {} /** diff --git a/lib/init/index.ts b/lib/init/index.ts deleted file mode 100644 index 812706c..0000000 --- a/lib/init/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ExpectedExports } from "../types"; - -declare const ActionProof: unique symbol; -export type ActionReceipt = { - [ActionProof]: never; -}; -export function noActions(): ActionReceipt { - return {} as ActionReceipt; -} - -declare const MigrationProof: unique symbol; -export type MigrationReceipt = { - [MigrationProof]: never; -}; -export function noMigrationsUp(): MigrationReceipt { - return {} as MigrationReceipt; -} -export function migrationUp(fn: () => Promise): MigrationReceipt { - fn(); - return {} as MigrationReceipt; -} - -declare const MigrationDownProof: unique symbol; -export type MigrationDownReceipt = { - [MigrationDownProof]: never; -}; -export function noMigrationsDown(): MigrationDownReceipt { - return {} as MigrationDownReceipt; -} -export function migrationDown( - fn: () => Promise, -): MigrationDownReceipt { - fn(); - return {} as MigrationDownReceipt; -} - -export function setupInit( - fn: ( - ...args: Parameters - ) => Promise<[MigrationReceipt, ActionReceipt]>, -) { - const initFn: ExpectedExports.init = (...args) => fn(...args); - return initFn; -} - -export function setupUninit( - fn: ( - ...args: Parameters - ) => Promise<[MigrationDownReceipt]>, -) { - const uninitFn: ExpectedExports.uninit = (...args) => fn(...args); - return uninitFn; -} diff --git a/lib/mainFn/index.ts b/lib/mainFn/index.ts index 61252ed..105b9d5 100644 --- a/lib/mainFn/index.ts +++ b/lib/mainFn/index.ts @@ -1,4 +1,5 @@ import { Effects, ExpectedExports } from "../types"; +import { Utils, utils } from "../util"; import { Daemons } from "./Daemons"; export * as network from "./exportInterfaces"; export { LocalBinding } from "./LocalBinding"; @@ -21,14 +22,18 @@ export { Daemons } from "./Daemons"; * @param fn * @returns */ -export const runningMain: ( +export const setupMain = ( fn: (o: { effects: Effects; started(onTerm: () => void): null; + utils: Utils; }) => Promise>, -) => ExpectedExports.main = (fn) => { +): ExpectedExports.main => { return async (options) => { - const result = await fn(options); + const result = await fn({ + ...options, + utils: utils(options.effects), + }); await result.build().then((x) => x.wait()); }; }; diff --git a/lib/manifest/ManifestTypes.ts b/lib/manifest/ManifestTypes.ts index 9137881..3e88de2 100644 --- a/lib/manifest/ManifestTypes.ts +++ b/lib/manifest/ManifestTypes.ts @@ -1,3 +1,5 @@ +import { ValidEmVer } from "../emverLite/mod"; + export interface Container { image: string; mounts: Record; @@ -5,10 +7,12 @@ export interface Container { sigtermTimeout?: string; // if more than 30s to shutdown } +export type ManifestVersion = ValidEmVer; + export interface GenericManifest { id: string; title: string; - version: string; + version: ManifestVersion; releaseNotes: string; license: string; // name of license replaces: string[]; diff --git a/lib/manifest/setupManifest.ts b/lib/manifest/setupManifest.ts index f301872..d5d7199 100644 --- a/lib/manifest/setupManifest.ts +++ b/lib/manifest/setupManifest.ts @@ -1,8 +1,9 @@ -import { GenericManifest } from "./ManifestTypes"; +import { GenericManifest, ManifestVersion } from "./ManifestTypes"; export function setupManifest< - M extends GenericManifest & { id: Id }, + M extends GenericManifest & { id: Id; version: Version }, Id extends string, + Version extends ManifestVersion, >(manifest: M): M { return manifest; } diff --git a/lib/migrations/Migration.ts b/lib/migrations/Migration.ts new file mode 100644 index 0000000..d17a893 --- /dev/null +++ b/lib/migrations/Migration.ts @@ -0,0 +1,28 @@ +import { ManifestVersion } from "../manifest/ManifestTypes"; +import { Effects } from "../types"; +import { Utils } from "../util"; + +export class Migration { + constructor( + readonly options: { + version: Version; + up: (opts: { effects: Effects }) => Promise; + down: (opts: { effects: Effects }) => Promise; + }, + ) {} + static of(options: { + version: Version; + up: (opts: { effects: Effects }) => Promise; + down: (opts: { effects: Effects }) => Promise; + }) { + return new Migration(options); + } + + async up(opts: { effects: Effects }) { + this.up(opts); + } + + async down(opts: { effects: Effects }) { + this.down(opts); + } +} diff --git a/lib/migrations/setupMigrations.ts b/lib/migrations/setupMigrations.ts new file mode 100644 index 0000000..89d2cfa --- /dev/null +++ b/lib/migrations/setupMigrations.ts @@ -0,0 +1,52 @@ +import { setupActions } from "../actions/setupActions"; +import { EmVer } from "../emverLite/mod"; +import { GenericManifest } from "../manifest/ManifestTypes"; +import { ExpectedExports } from "../types"; +import { once } from "../util/once"; +import { Migration } from "./Migration"; + +export function setupMigrations>>( + manifest: GenericManifest, + initializeActions: ReturnType["initializeActions"], + ...migrations: EnsureUniqueId +) { + const sortedMigrations = once(() => { + const migrationsAsVersions = (migrations as Array>).map( + (x) => [EmVer.parse(x.options.version), x] as const, + ); + migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])); + return migrationsAsVersions; + }); + const currentVersion = once(() => EmVer.parse(manifest.version)); + const init: ExpectedExports.init = async ({ effects, previousVersion }) => { + await initializeActions(effects); + if (!!previousVersion) { + const previousVersionEmVer = EmVer.parse(previousVersion); + for (const [_, migration] of sortedMigrations() + .filter((x) => x[0].greaterThan(previousVersionEmVer)) + .filter((x) => x[0].lessThanOrEqual(currentVersion()))) { + await migration.up({ effects }); + } + } + }; + const uninit: ExpectedExports.uninit = async ({ effects, nextVersion }) => { + if (!!nextVersion) { + const nextVersionEmVer = EmVer.parse(nextVersion); + const reversed = [...sortedMigrations()].reverse(); + for (const [_, migration] of reversed + .filter((x) => x[0].greaterThan(nextVersionEmVer)) + .filter((x) => x[0].lessThanOrEqual(currentVersion()))) { + await migration.down({ effects }); + } + } + }; + return { init, uninit }; +} + +// prettier-ignore +export type EnsureUniqueId = + B extends [] ? A : + B extends [Migration, ...infer Rest] ? ( + id extends ids ? "One of the ids are not unique"[] : + EnsureUniqueId + ) : "There exists a migration that is not a Migration"[] diff --git a/lib/properties/index.ts b/lib/properties/index.ts index 4af6395..cb0d363 100644 --- a/lib/properties/index.ts +++ b/lib/properties/index.ts @@ -1,4 +1,5 @@ import { ExpectedExports, Properties } from "../types"; +import { Utils, utils } from "../util"; import "../util/extensions"; import { PropertyGroup } from "./PropertyGroup"; import { PropertyString } from "./PropertyString"; @@ -18,13 +19,17 @@ export type UnionToIntersection = ((x: T) => any) extends (x: infer R) => any * @param fn * @returns */ -export function setupPropertiesExport( - fn: ( - ...args: Parameters - ) => void | Promise | Promise<(PropertyGroup | PropertyString)[]>, +export function setupProperties( + fn: (args: { + wrapperData: WrapperData; + }) => void | Promise | Promise<(PropertyGroup | PropertyString)[]>, ): ExpectedExports.properties { - return (async (...args) => { - const result = await fn(...args); + return (async (options) => { + const result = await fn( + options as { + wrapperData: WrapperData & typeof options.wrapperData; + }, + ); if (result) { const answer: Properties = result.map((x) => x.data); return answer; diff --git a/lib/types.ts b/lib/types.ts index 993efb6..8a00e5b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,7 +1,6 @@ export * as configTypes from "./config/configTypes"; import { InputSpec } from "./config/configTypes"; import { DependenciesReceipt } from "./config/setupConfig"; -import { ActionReceipt } from "./init"; export type ExportedAction = (options: { effects: Effects; @@ -350,7 +349,7 @@ export type Effects = { * * @param options */ - exportAction(options: ActionMetaData): Promise; + exportAction(options: ActionMetaData): Promise; /** * Remove an action that was exported. Used problably during main or during setConfig. */ diff --git a/lib/util/index.ts b/lib/util/index.ts index 2754e2e..d808f3e 100644 --- a/lib/util/index.ts +++ b/lib/util/index.ts @@ -17,6 +17,7 @@ export { FileHelper } from "./fileHelper"; export { getWrapperData } from "./getWrapperData"; export { deepEqual } from "./deepEqual"; export { deepMerge } from "./deepMerge"; +export { once } from "./once"; /** Used to check if the file exists before hand */ export const exists = ( diff --git a/lib/util/once.ts b/lib/util/once.ts new file mode 100644 index 0000000..d4a55e5 --- /dev/null +++ b/lib/util/once.ts @@ -0,0 +1,9 @@ +export function once(fn: () => B): () => B { + let result: [B] | [] = []; + return () => { + if (!result.length) { + result = [fn()]; + } + return result[0]; + }; +} diff --git a/package-lock.json b/package-lock.json index 2cfd4a1..2fdd63f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "start-sdk", - "version": "0.4.0-lib0.charlie33", + "version": "0.4.0-lib0.charlie34", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "start-sdk", - "version": "0.4.0-lib0.charlie33", + "version": "0.4.0-lib0.charlie34", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 6e81fd4..6379cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "start-sdk", - "version": "0.4.0-lib0.charlie33", + "version": "0.4.0-lib0.charlie34", "description": "For making the patterns that are wanted in making services for the startOS.", "main": "./lib/index.js", "types": "./lib/index.d.ts",