@gadgetinc/ggt 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -76
- package/bin/dev.js +4 -7
- package/lib/__generated__/graphql.js.map +1 -1
- package/lib/commands/deploy.js +227 -0
- package/lib/commands/deploy.js.map +1 -0
- package/lib/commands/list.js +28 -21
- package/lib/commands/list.js.map +1 -1
- package/lib/commands/login.js +22 -20
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/logout.js +13 -9
- package/lib/commands/logout.js.map +1 -1
- package/lib/commands/root.js +89 -56
- package/lib/commands/root.js.map +1 -1
- package/lib/commands/sync.js +256 -499
- package/lib/commands/sync.js.map +1 -1
- package/lib/commands/version.js +21 -0
- package/lib/commands/version.js.map +1 -0
- package/lib/commands/whoami.js +15 -11
- package/lib/commands/whoami.js.map +1 -1
- package/lib/main.js +4 -10
- package/lib/main.js.map +1 -1
- package/lib/services/{app.js → app/app.js} +9 -5
- package/lib/services/app/app.js.map +1 -0
- package/lib/services/app/arg.js +28 -0
- package/lib/services/app/arg.js.map +1 -0
- package/lib/services/app/edit-graphql.js +389 -0
- package/lib/services/app/edit-graphql.js.map +1 -0
- package/lib/services/command/arg.js +53 -0
- package/lib/services/command/arg.js.map +1 -0
- package/lib/services/command/command.js +27 -0
- package/lib/services/command/command.js.map +1 -0
- package/lib/services/command/context.js +60 -0
- package/lib/services/command/context.js.map +1 -0
- package/lib/services/{config.js → config/config.js} +32 -35
- package/lib/services/config/config.js.map +1 -0
- package/lib/services/config/env.js +22 -0
- package/lib/services/config/env.js.map +1 -0
- package/lib/services/config/package-json.js +9 -0
- package/lib/services/config/package-json.js.map +1 -0
- package/lib/services/filesync/changes.js +97 -0
- package/lib/services/filesync/changes.js.map +1 -0
- package/lib/services/filesync/conflicts.js +137 -0
- package/lib/services/filesync/conflicts.js.map +1 -0
- package/lib/services/filesync/directory.js +253 -0
- package/lib/services/filesync/directory.js.map +1 -0
- package/lib/services/filesync/error.js +67 -0
- package/lib/services/filesync/error.js.map +1 -0
- package/lib/services/filesync/file.js +3 -0
- package/lib/services/filesync/file.js.map +1 -0
- package/lib/services/filesync/filesync.js +675 -0
- package/lib/services/filesync/filesync.js.map +1 -0
- package/lib/services/filesync/hashes.js +150 -0
- package/lib/services/filesync/hashes.js.map +1 -0
- package/lib/services/http/auth.js +41 -0
- package/lib/services/http/auth.js.map +1 -0
- package/lib/services/http/http.js +64 -0
- package/lib/services/http/http.js.map +1 -0
- package/lib/services/output/log/field.js +3 -0
- package/lib/services/output/log/field.js.map +1 -0
- package/lib/services/output/log/format/format.js +8 -0
- package/lib/services/output/log/format/format.js.map +1 -0
- package/lib/services/output/log/format/json.js +45 -0
- package/lib/services/output/log/format/json.js.map +1 -0
- package/lib/services/output/log/format/pretty.js +147 -0
- package/lib/services/output/log/format/pretty.js.map +1 -0
- package/lib/services/output/log/level.js +41 -0
- package/lib/services/output/log/level.js.map +1 -0
- package/lib/services/output/log/logger.js +40 -0
- package/lib/services/output/log/logger.js.map +1 -0
- package/lib/services/output/log/printer.js +120 -0
- package/lib/services/output/log/printer.js.map +1 -0
- package/lib/services/output/log/structured.js +52 -0
- package/lib/services/output/log/structured.js.map +1 -0
- package/lib/services/{notify.js → output/notify.js} +7 -6
- package/lib/services/output/notify.js.map +1 -0
- package/lib/services/output/prompt.js +52 -0
- package/lib/services/output/prompt.js.map +1 -0
- package/lib/services/output/report.js +162 -0
- package/lib/services/output/report.js.map +1 -0
- package/lib/services/output/sprint.js +21 -0
- package/lib/services/output/sprint.js.map +1 -0
- package/lib/services/{output.js → output/stream.js} +18 -23
- package/lib/services/output/stream.js.map +1 -0
- package/lib/services/{version.js → output/update.js} +26 -18
- package/lib/services/output/update.js.map +1 -0
- package/lib/services/user/session.js +50 -0
- package/lib/services/user/session.js.map +1 -0
- package/lib/services/{user.js → user/user.js} +24 -17
- package/lib/services/user/user.js.map +1 -0
- package/lib/services/util/boolean.js +15 -0
- package/lib/services/util/boolean.js.map +1 -0
- package/lib/services/util/collection.js +38 -0
- package/lib/services/util/collection.js.map +1 -0
- package/lib/services/util/function.js +97 -0
- package/lib/services/util/function.js.map +1 -0
- package/lib/services/util/is.js +46 -0
- package/lib/services/util/is.js.map +1 -0
- package/lib/services/util/number.js +27 -0
- package/lib/services/util/number.js.map +1 -0
- package/lib/services/util/object.js +101 -0
- package/lib/services/util/object.js.map +1 -0
- package/lib/services/util/paths.js +36 -0
- package/lib/services/util/paths.js.map +1 -0
- package/lib/services/{promise.js → util/promise.js} +4 -4
- package/lib/services/util/promise.js.map +1 -0
- package/npm-shrinkwrap.json +2416 -1547
- package/package.json +52 -46
- package/lib/commands/index.js +0 -9
- package/lib/commands/index.js.map +0 -1
- package/lib/services/app.js.map +0 -1
- package/lib/services/args.js +0 -28
- package/lib/services/args.js.map +0 -1
- package/lib/services/config.js.map +0 -1
- package/lib/services/edit-graphql.js +0 -193
- package/lib/services/edit-graphql.js.map +0 -1
- package/lib/services/errors.js +0 -274
- package/lib/services/errors.js.map +0 -1
- package/lib/services/filesync.js +0 -404
- package/lib/services/filesync.js.map +0 -1
- package/lib/services/fs-utils.js +0 -33
- package/lib/services/fs-utils.js.map +0 -1
- package/lib/services/http.js +0 -53
- package/lib/services/http.js.map +0 -1
- package/lib/services/log.js +0 -45
- package/lib/services/log.js.map +0 -1
- package/lib/services/notify.js.map +0 -1
- package/lib/services/output.js.map +0 -1
- package/lib/services/promise.js.map +0 -1
- package/lib/services/session.js +0 -27
- package/lib/services/session.js.map +0 -1
- package/lib/services/sleep.js +0 -19
- package/lib/services/sleep.js.map +0 -1
- package/lib/services/timeout.js +0 -8
- package/lib/services/timeout.js.map +0 -1
- package/lib/services/user.js.map +0 -1
- package/lib/services/version.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/services/filesync/filesync.ts"],"sourcesContent":["import dayjs from \"dayjs\";\nimport { findUp } from \"find-up\";\nimport fs from \"fs-extra\";\nimport ms from \"ms\";\nimport assert from \"node:assert\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport pMap from \"p-map\";\nimport PQueue from \"p-queue\";\nimport pRetry from \"p-retry\";\nimport type { Promisable } from \"type-fest\";\nimport { z } from \"zod\";\nimport { FileSyncEncoding, type FileSyncChangedEventInput, type FileSyncDeletedEventInput } from \"../../__generated__/graphql.js\";\nimport type { App } from \"../app/app.js\";\nimport { getApps } from \"../app/app.js\";\nimport {\n EditGraphQL,\n FILE_SYNC_COMPARISON_HASHES_QUERY,\n FILE_SYNC_FILES_QUERY,\n FILE_SYNC_HASHES_QUERY,\n PUBLISH_FILE_SYNC_EVENTS_MUTATION,\n REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION,\n} from \"../app/edit-graphql.js\";\nimport { ArgError } from \"../command/arg.js\";\nimport { config, homePath } from \"../config/config.js\";\nimport { createLogger } from \"../output/log/logger.js\";\nimport { select } from \"../output/prompt.js\";\nimport { sprint } from \"../output/sprint.js\";\nimport type { User } from \"../user/user.js\";\nimport { sortBySimilar } from \"../util/collection.js\";\nimport { noop } from \"../util/function.js\";\nimport { Changes, printChanges } from \"./changes.js\";\nimport { getConflicts, printConflicts, withoutConflictingChanges } from \"./conflicts.js\";\nimport { Directory, supportsPermissions, swallowEnoent, type Hashes } from \"./directory.js\";\nimport { InvalidSyncFileError, TooManySyncAttemptsError } from \"./error.js\";\nimport type { File } from \"./file.js\";\nimport { getChanges, isEqualHashes, type ChangesWithHash } from \"./hashes.js\";\n\nexport class FileSync {\n readonly editGraphQL: EditGraphQL;\n\n readonly log = createLogger({ name: \"filesync\", fields: () => ({ state: this._state }) });\n\n /**\n * A FIFO async callback queue that ensures we process filesync events\n * in the order we receive them.\n */\n private _queue = new PQueue({ concurrency: 1 });\n\n private constructor(\n /**\n * The directory that is being synced to.\n */\n readonly directory: Directory,\n\n /**\n * Whether the directory was empty or non-existent when we started.\n */\n readonly wasEmptyOrNonExistent: boolean,\n\n /**\n * The Gadget application that is being synced to.\n */\n readonly app: App,\n\n /**\n * The state of the filesystem.\n *\n * This is persisted to `.gadget/sync.json` within the {@linkcode directory}.\n */\n private _state: { app: string; filesVersion: string; mtime: number },\n ) {\n this.editGraphQL = new EditGraphQL(this.app);\n }\n\n /**\n * The last filesVersion that was written to the filesystem.\n *\n * This determines if the filesystem in Gadget is ahead of the\n * filesystem on the local machine.\n */\n get filesVersion(): bigint {\n return BigInt(this._state.filesVersion);\n }\n\n /**\n * The largest mtime that was seen on the filesystem.\n *\n * This is used to determine if any files have changed since the last\n * sync. This does not include the mtime of files that are ignored.\n */\n get mtime(): number {\n return this._state.mtime;\n }\n\n /**\n * Initializes a {@linkcode FileSync} instance.\n * - Ensures the directory exists.\n * - Ensures the directory is empty or contains a `.gadget/sync.json` file (unless `options.force` is `true`)\n * - Ensures an app is specified (either via `options.app` or by prompting the user)\n * - Ensures the specified app matches the app the directory was previously synced to (unless `options.force` is `true`)\n */\n static async init(options: { user: User; dir?: string; app?: string; force?: boolean }): Promise<FileSync> {\n const apps = await getApps(options.user);\n if (apps.length === 0) {\n throw new ArgError(\n sprint`\n You (${options.user.email}) don't have have any Gadget applications.\n\n Visit https://gadget.new to create one!\n `,\n );\n }\n\n let dir = options.dir;\n if (!dir) {\n // the user didn't specify a directory\n const filepath = await findUp(\".gadget/sync.json\");\n if (filepath) {\n // we found a .gadget/sync.json file, use its parent directory\n dir = path.join(filepath, \"../..\");\n } else {\n // we didn't find a .gadget/sync.json file, use the current directory\n dir = process.cwd();\n }\n }\n\n if (config.windows && dir.startsWith(\"~/\")) {\n // `~` doesn't expand to the home directory on Windows\n dir = homePath(dir.slice(2));\n }\n\n // ensure the root directory is an absolute path and exists\n const wasEmptyOrNonExistent = await isEmptyOrNonExistentDir(dir);\n await fs.ensureDir((dir = path.resolve(dir)));\n\n // try to load the .gadget/sync.json file\n const state = await fs\n .readJson(path.join(dir, \".gadget/sync.json\"))\n .then((json) =>\n z\n .object({\n app: z.string(),\n filesVersion: z.string(),\n mtime: z.number(),\n })\n .parse(json),\n )\n .catch(noop);\n\n let appSlug = options.app || state?.app;\n if (!appSlug) {\n // the user didn't specify an app, suggest some apps that they can sync to\n appSlug = await select({\n message: \"Select the app to sync to\",\n choices: apps.map((x) => x.slug),\n });\n }\n\n // try to find the appSlug in their list of apps\n const app = apps.find((app) => app.slug === appSlug);\n if (!app) {\n // the specified appSlug doesn't exist in their list of apps,\n // either they misspelled it or they don't have access to it\n // anymore, suggest some apps that are similar to the one they\n // specified\n const similarAppSlugs = sortBySimilar(\n appSlug,\n apps.map((app) => app.slug),\n ).slice(0, 5);\n\n throw new ArgError(\n sprint`\n Unknown application:\n\n ${appSlug}\n\n Did you mean one of these?\n\n\n `.concat(` • ${similarAppSlugs.join(\"\\n • \")}`),\n );\n }\n\n const directory = await Directory.init(dir);\n\n if (!state) {\n // the .gadget/sync.json file didn't exist or contained invalid json\n if (wasEmptyOrNonExistent || options.force) {\n // the directory was empty or the user passed --force\n // either way, create a fresh .gadget/sync.json file\n return new FileSync(directory, wasEmptyOrNonExistent, app, { app: app.slug, filesVersion: \"0\", mtime: 0 });\n }\n\n // the directory isn't empty and the user didn't pass --force\n throw new InvalidSyncFileError(dir, app.slug);\n }\n\n // the .gadget/sync.json file exists\n if (state.app === app.slug) {\n // the .gadget/sync.json file is for the same app that the user specified\n return new FileSync(directory, wasEmptyOrNonExistent, app, state);\n }\n\n // the .gadget/sync.json file is for a different app\n if (options.force) {\n // the user passed --force, so use the app they specified and overwrite everything\n return new FileSync(directory, wasEmptyOrNonExistent, app, { app: app.slug, filesVersion: \"0\", mtime: 0 });\n }\n\n // the user didn't pass --force, so throw an error\n throw new ArgError(sprint`\n You were about to sync the following app to the following directory:\n\n {dim ${app.slug}} → {dim ${dir}}\n\n However, that directory has already been synced with this app:\n\n {dim ${state.app}}\n\n If you're sure that you want to sync:\n\n {dim ${app.slug}} → {dim ${dir}}\n\n Then run {dim ggt sync} again with the {dim --force} flag.\n `);\n }\n\n /**\n * Waits for all pending and ongoing filesync operations to complete.\n */\n async idle(): Promise<void> {\n await this._queue.onIdle();\n }\n\n /**\n * Sends file changes to the Gadget.\n *\n * @param changes - The changes to send.\n * @returns A promise that resolves when the changes have been sent.\n */\n async sendChangesToGadget({ changes }: { changes: Changes }): Promise<void> {\n await this._enqueue(() => this._sendChangesToGadget({ changes }));\n }\n\n /**\n * Subscribes to file changes on Gadget and executes the provided\n * callbacks before and after the changes occur.\n *\n * @returns A function that unsubscribes from changes on Gadget.\n */\n subscribeToGadgetChanges({\n beforeChanges,\n afterChanges,\n onError,\n }: {\n beforeChanges: (data: { changed: string[]; deleted: string[] }) => Promisable<void>;\n afterChanges: (data: { changes: Changes }) => Promisable<void>;\n onError: (error: unknown) => void;\n }): () => void {\n return this.editGraphQL.subscribe({\n query: REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION,\n // the reason this is a function rather than a static value is\n // so that it will be re-evaluated if the connection is lost and\n // then re-established. this ensures that we send our current\n // filesVersion rather than the one that was sent when we first\n // subscribed\n variables: () => ({ localFilesVersion: String(this.filesVersion) }),\n onError,\n onData: ({ remoteFileSyncEvents: { changed, deleted, remoteFilesVersion } }) => {\n this._enqueue(async () => {\n if (BigInt(remoteFilesVersion) < this.filesVersion) {\n this.log.warn(\"skipping received changes because files version is outdated\", { filesVersion: remoteFilesVersion });\n return;\n }\n\n this.log.debug(\"received files\", {\n remoteFilesVersion: remoteFilesVersion,\n changed: changed.map((change) => change.path),\n deleted: deleted.map((change) => change.path),\n });\n\n const filterIgnoredFiles = (file: { path: string }): boolean => {\n const ignored = this.directory.ignores(file.path);\n if (ignored) {\n this.log.warn(\"skipping received change because file is ignored\", { path: file.path });\n }\n return !ignored;\n };\n\n changed = changed.filter(filterIgnoredFiles);\n deleted = deleted.filter(filterIgnoredFiles);\n\n if (changed.length === 0 && deleted.length === 0) {\n await this._save(remoteFilesVersion);\n return;\n }\n\n await beforeChanges({\n changed: changed.map((file) => file.path),\n deleted: deleted.map((file) => file.path),\n });\n\n const changes = await this._writeToLocalFilesystem({\n filesVersion: remoteFilesVersion,\n files: changed,\n delete: deleted.map((file) => file.path),\n });\n\n if (changes.size > 0) {\n printChanges({\n message: sprint`← Received {gray ${dayjs().format(\"hh:mm:ss A\")}}`,\n changes,\n tense: \"past\",\n limit: 10,\n });\n }\n\n await afterChanges({ changes });\n }).catch(onError);\n },\n });\n }\n\n /**\n * Ensures the local filesystem is in sync with Gadget's filesystem.\n * - All non-conflicting changes are automatically merged.\n * - Conflicts are resolved by prompting the user to either keep their\n * local changes or keep Gadget's changes.\n * - This function will not return until the filesystem is in sync.\n */\n async sync({ attempt = 0, preference }: { attempt?: number; preference?: ConflictPreference } = {}): Promise<void> {\n if (attempt > 10) {\n throw new TooManySyncAttemptsError(attempt);\n }\n\n const { filesVersionHashes, localHashes, gadgetHashes, gadgetFilesVersion } = await this._getHashes();\n this.log.debug(\"syncing\", { filesVersionHashes, localHashes, gadgetHashes, gadgetFilesVersion });\n\n if (isEqualHashes(localHashes, gadgetHashes)) {\n this.log.info(\"filesystem is in sync\");\n await this._save(gadgetFilesVersion);\n return;\n }\n\n let localChanges = getChanges({ from: filesVersionHashes, to: localHashes, existing: gadgetHashes, ignore: [\".gadget/\"] });\n let gadgetChanges = getChanges({ from: filesVersionHashes, to: gadgetHashes, existing: localHashes });\n\n if (localChanges.size === 0 && gadgetChanges.size === 0) {\n // the local filesystem is missing .gadget/ files\n gadgetChanges = getChanges({ from: localHashes, to: gadgetHashes });\n assertAllGadgetFiles({ gadgetChanges });\n }\n\n const conflicts = getConflicts({ localChanges, gadgetChanges });\n if (conflicts.size > 0) {\n this.log.debug(\"conflicts detected\", { conflicts });\n\n if (!preference) {\n printConflicts({\n message: sprint`{bold You have conflicting changes with Gadget}`,\n conflicts,\n });\n\n preference = await select({\n message: \"How would you like to resolve these conflicts?\",\n choices: Object.values(ConflictPreference),\n });\n }\n\n switch (preference) {\n case ConflictPreference.CANCEL: {\n process.exit(0);\n break;\n }\n case ConflictPreference.LOCAL: {\n gadgetChanges = withoutConflictingChanges({ conflicts, changes: gadgetChanges });\n break;\n }\n case ConflictPreference.GADGET: {\n localChanges = withoutConflictingChanges({ conflicts, changes: localChanges });\n break;\n }\n }\n }\n\n assert(localChanges.size > 0 || gadgetChanges.size > 0, \"there must be changes if hashes don't match\");\n\n if (gadgetChanges.size > 0) {\n await this._getChangesFromGadget({ changes: gadgetChanges, filesVersion: gadgetFilesVersion });\n }\n\n if (localChanges.size > 0) {\n await this._sendChangesToGadget({ changes: localChanges, expectedFilesVersion: gadgetFilesVersion });\n }\n\n // recursively call this function until we're in sync\n return this.sync({ attempt: ++attempt, preference });\n }\n\n async _getHashes(): Promise<{\n gadgetFilesVersion: bigint;\n filesVersionHashes: Hashes;\n localHashes: Hashes;\n gadgetHashes: Hashes;\n }> {\n const [localHashes, { filesVersionHashes, gadgetHashes, gadgetFilesVersion }] = await Promise.all([\n // get the hashes of our local files\n this.directory.hashes(),\n // get the hashes of our local filesVersion and the latest filesVersion\n (async () => {\n let gadgetFilesVersion: bigint;\n let gadgetHashes: Hashes;\n let filesVersionHashes: Hashes;\n\n if (this.filesVersion === 0n) {\n // this is the first time we're syncing, so just get the\n // hashes of the latest filesVersion\n const { fileSyncHashes } = await this.editGraphQL.query({ query: FILE_SYNC_HASHES_QUERY });\n gadgetFilesVersion = BigInt(fileSyncHashes.filesVersion);\n gadgetHashes = fileSyncHashes.hashes;\n filesVersionHashes = {};\n } else {\n // this isn't the first time we're syncing, so get the hashes\n // of the files at our local filesVersion and the latest\n // filesVersion\n const { fileSyncComparisonHashes } = await this.editGraphQL.query({\n query: FILE_SYNC_COMPARISON_HASHES_QUERY,\n variables: { filesVersion: String(this.filesVersion) },\n });\n gadgetFilesVersion = BigInt(fileSyncComparisonHashes.latestFilesVersionHashes.filesVersion);\n gadgetHashes = fileSyncComparisonHashes.latestFilesVersionHashes.hashes;\n filesVersionHashes = fileSyncComparisonHashes.filesVersionHashes.hashes;\n }\n\n return { filesVersionHashes, gadgetHashes, gadgetFilesVersion };\n })(),\n ]);\n\n return { filesVersionHashes, localHashes, gadgetHashes, gadgetFilesVersion };\n }\n\n private async _getChangesFromGadget({\n filesVersion,\n changes,\n }: {\n filesVersion: bigint;\n changes: Changes | ChangesWithHash;\n }): Promise<void> {\n this.log.debug(\"getting changes from gadget\", { filesVersion, changes });\n const created = changes.created();\n const updated = changes.updated();\n\n let files: File[] = [];\n if (created.length > 0 || updated.length > 0) {\n const { fileSyncFiles } = await this.editGraphQL.query({\n query: FILE_SYNC_FILES_QUERY,\n variables: {\n paths: [...created, ...updated],\n filesVersion: String(filesVersion),\n encoding: FileSyncEncoding.Base64,\n },\n });\n\n files = fileSyncFiles.files;\n }\n\n await this._writeToLocalFilesystem({\n filesVersion,\n files,\n delete: changes.deleted(),\n });\n\n printChanges({\n changes,\n tense: \"past\",\n message: sprint`← Received {gray ${dayjs().format(\"hh:mm:ss A\")}}`,\n });\n }\n\n private async _sendChangesToGadget({\n expectedFilesVersion = this.filesVersion,\n changes,\n printLimit,\n }: {\n expectedFilesVersion?: bigint;\n changes: Changes;\n printLimit?: number;\n }): Promise<void> {\n this.log.debug(\"sending changes to gadget\", { expectedFilesVersion, changes });\n const changed: FileSyncChangedEventInput[] = [];\n const deleted: FileSyncDeletedEventInput[] = [];\n\n await pMap(changes, async ([normalizedPath, change]) => {\n if (change.type === \"delete\") {\n deleted.push({ path: normalizedPath });\n return;\n }\n\n const absolutePath = this.directory.absolute(normalizedPath);\n\n let stats;\n try {\n stats = await fs.stat(absolutePath);\n } catch (error) {\n swallowEnoent(error);\n this.log.debug(\"skipping change because file doesn't exist\", { path: normalizedPath });\n return;\n }\n\n let content = \"\";\n if (stats.isFile()) {\n content = await fs.readFile(absolutePath, FileSyncEncoding.Base64);\n }\n\n let oldPath;\n if (change.type === \"create\" && change.oldPath) {\n oldPath = change.oldPath;\n }\n\n changed.push({\n content,\n oldPath,\n path: normalizedPath,\n mode: stats.mode,\n encoding: FileSyncEncoding.Base64,\n });\n });\n\n if (changed.length === 0 && deleted.length === 0) {\n this.log.debug(\"skipping send because there are no changes\");\n return;\n }\n\n const {\n publishFileSyncEvents: { remoteFilesVersion },\n } = await this.editGraphQL.query({\n query: PUBLISH_FILE_SYNC_EVENTS_MUTATION,\n variables: {\n input: {\n expectedRemoteFilesVersion: String(expectedFilesVersion),\n changed,\n deleted,\n },\n },\n });\n\n await this._save(remoteFilesVersion);\n\n printChanges({\n changes,\n tense: \"past\",\n message: sprint`→ Sent {gray ${dayjs().format(\"hh:mm:ss A\")}}`,\n limit: printLimit,\n });\n }\n\n private async _writeToLocalFilesystem(options: { filesVersion: bigint | string; files: File[]; delete: string[] }): Promise<Changes> {\n const filesVersion = BigInt(options.filesVersion);\n assert(filesVersion >= this.filesVersion, \"filesVersion must be greater than or equal to current filesVersion\");\n\n this.log.debug(\"writing to local filesystem\", {\n filesVersion,\n files: options.files.map((file) => file.path),\n delete: options.delete,\n });\n\n const created: string[] = [];\n const updated: string[] = [];\n\n await pMap(options.delete, async (filepath) => {\n const currentPath = this.directory.absolute(filepath);\n const backupPath = this.directory.absolute(\".gadget/backup\", this.directory.relative(filepath));\n\n // rather than `rm -rf`ing files, we move them to\n // `.gadget/backup/` so that users can recover them if something\n // goes wrong. We've seen a lot of EBUSY/EINVAL errors when moving\n // files so we retry a few times.\n await pRetry(\n async () => {\n try {\n // remove the current backup file in case it exists and is a\n // different type (file vs directory)\n await fs.remove(backupPath);\n await fs.move(currentPath, backupPath);\n } catch (error) {\n // replicate the behavior of `rm -rf` and ignore ENOENT\n swallowEnoent(error);\n }\n },\n {\n retries: 2,\n minTimeout: ms(\"100ms\"),\n onFailedAttempt: (error) => {\n this.log.warn(\"failed to move file to backup\", { error, currentPath, backupPath });\n },\n },\n );\n });\n\n await pMap(options.files, async (file) => {\n const absolutePath = this.directory.absolute(file.path);\n if (await fs.pathExists(absolutePath)) {\n updated.push(file.path);\n } else {\n created.push(file.path);\n }\n\n if (file.path.endsWith(\"/\")) {\n await fs.ensureDir(absolutePath);\n } else {\n await fs.outputFile(absolutePath, Buffer.from(file.content, file.encoding));\n }\n\n if (supportsPermissions) {\n // the os's default umask makes setting the mode during creation\n // not work, so an additional fs.chmod call is necessary to\n // ensure the file has the correct mode\n await fs.chmod(absolutePath, file.mode & 0o777);\n }\n\n if (absolutePath === this.directory.absolute(\".ignore\")) {\n await this.directory.loadIgnoreFile();\n }\n });\n\n await this._save(String(filesVersion));\n\n return new Changes([\n ...created.map((path) => [path, { type: \"create\" }] as const),\n ...updated.map((path) => [path, { type: \"update\" }] as const),\n ...options.delete.map((path) => [path, { type: \"delete\" }] as const),\n ]);\n }\n\n /**\n * Updates {@linkcode _state} and saves it to `.gadget/sync.json`.\n */\n private async _save(filesVersion: string | bigint): Promise<void> {\n this._state = { ...this._state, mtime: Date.now() + 1, filesVersion: String(filesVersion) };\n this.log.debug(\"saving state\", { state: this._state });\n await fs.outputJSON(this.directory.absolute(\".gadget/sync.json\"), this._state, { spaces: 2 });\n }\n\n /**\n * Enqueues a function that handles filesync events onto the {@linkcode _queue}.\n */\n private _enqueue<T>(fn: () => Promise<T>): Promise<T> {\n return this._queue.add(fn) as Promise<T>;\n }\n}\n\n/**\n * Checks if a directory is empty or non-existent.\n *\n * @param dir - The directory path to check.\n * @returns A Promise that resolves to a boolean indicating whether the directory is empty or non-existent.\n */\nexport const isEmptyOrNonExistentDir = async (dir: string): Promise<boolean> => {\n try {\n for await (const _ of await fs.opendir(dir, { bufferSize: 1 })) {\n return false;\n }\n return true;\n } catch (error) {\n swallowEnoent(error);\n return true;\n }\n};\n\nexport const assertAllGadgetFiles = ({ gadgetChanges }: { gadgetChanges: Changes }): void => {\n assert(gadgetChanges.created().length > 0, \"expected gadgetChanges to have created files\");\n assert(gadgetChanges.deleted().length === 0, \"expected gadgetChanges to not have deleted files\");\n assert(gadgetChanges.updated().length === 0, \"expected gadgetChanges to not have updated files\");\n\n const allGadgetFiles = Array.from(gadgetChanges.keys()).every((path) => path.startsWith(\".gadget/\"));\n assert(allGadgetFiles, \"expected all gadgetChanges to be .gadget/ files\");\n};\n\nexport const ConflictPreference = Object.freeze({\n CANCEL: \"Cancel (Ctrl+C)\",\n LOCAL: \"Keep my conflicting changes\",\n GADGET: \"Keep Gadget's conflicting changes\",\n});\n\nexport type ConflictPreference = (typeof ConflictPreference)[keyof typeof ConflictPreference];\n\nexport const ConflictPreferenceArg = (value: string, name: string): ConflictPreference => {\n if ([\"local\", \"gadget\"].includes(value)) {\n return ConflictPreference[value.toUpperCase() as keyof typeof ConflictPreference];\n }\n\n throw new ArgError(sprint`\n ${name} must be {bold local} or {bold gadget}\n\n {bold EXAMPLES:}\n ${name} local\n ${name} gadget\n `);\n};\n"],"names":["dayjs","findUp","fs","ms","assert","path","process","pMap","PQueue","pRetry","z","FileSyncEncoding","getApps","EditGraphQL","FILE_SYNC_COMPARISON_HASHES_QUERY","FILE_SYNC_FILES_QUERY","FILE_SYNC_HASHES_QUERY","PUBLISH_FILE_SYNC_EVENTS_MUTATION","REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION","ArgError","config","homePath","createLogger","select","sprint","sortBySimilar","noop","Changes","printChanges","getConflicts","printConflicts","withoutConflictingChanges","Directory","supportsPermissions","swallowEnoent","InvalidSyncFileError","TooManySyncAttemptsError","getChanges","isEqualHashes","FileSync","filesVersion","BigInt","_state","mtime","init","options","apps","user","length","email","dir","filepath","join","cwd","windows","startsWith","slice","wasEmptyOrNonExistent","isEmptyOrNonExistentDir","ensureDir","resolve","state","readJson","then","json","object","app","string","number","parse","catch","appSlug","message","choices","map","x","slug","find","similarAppSlugs","concat","directory","force","idle","_queue","onIdle","sendChangesToGadget","changes","_enqueue","_sendChangesToGadget","subscribeToGadgetChanges","beforeChanges","afterChanges","onError","editGraphQL","subscribe","query","variables","localFilesVersion","String","onData","remoteFileSyncEvents","changed","deleted","remoteFilesVersion","log","warn","debug","change","filterIgnoredFiles","file","ignored","ignores","filter","_save","_writeToLocalFilesystem","files","delete","size","format","tense","limit","sync","attempt","preference","filesVersionHashes","localHashes","gadgetHashes","gadgetFilesVersion","_getHashes","info","localChanges","from","to","existing","ignore","gadgetChanges","assertAllGadgetFiles","conflicts","Object","values","ConflictPreference","CANCEL","exit","LOCAL","GADGET","_getChangesFromGadget","expectedFilesVersion","Promise","all","hashes","fileSyncHashes","fileSyncComparisonHashes","latestFilesVersionHashes","created","updated","fileSyncFiles","paths","encoding","Base64","printLimit","normalizedPath","type","push","absolutePath","absolute","stats","stat","error","content","isFile","readFile","oldPath","mode","publishFileSyncEvents","input","expectedRemoteFilesVersion","currentPath","backupPath","relative","remove","move","retries","minTimeout","onFailedAttempt","pathExists","endsWith","outputFile","Buffer","chmod","loadIgnoreFile","Date","now","outputJSON","spaces","fn","add","name","fields","concurrency","_","opendir","bufferSize","allGadgetFiles","Array","keys","every","freeze","ConflictPreferenceArg","value","includes","toUpperCase"],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { createLogger } from "../output/log/logger.js";
|
|
3
|
+
import { isNil } from "../util/is.js";
|
|
4
|
+
const log = createLogger({
|
|
5
|
+
name: "hashes"
|
|
6
|
+
});
|
|
7
|
+
export class ChangesWithHash extends Map {
|
|
8
|
+
created() {
|
|
9
|
+
return Array.from(this.entries()).filter(([, change])=>change.type === "create").map(([path])=>path);
|
|
10
|
+
}
|
|
11
|
+
updated() {
|
|
12
|
+
return Array.from(this.entries()).filter(([, change])=>change.type === "update").map(([path])=>path);
|
|
13
|
+
}
|
|
14
|
+
deleted() {
|
|
15
|
+
return Array.from(this.entries()).filter(([, change])=>change.type === "delete").map(([path])=>path);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calculates the changes that were made to `from` to make it end up as `to`.
|
|
20
|
+
*
|
|
21
|
+
* If `existing` is provided, only the changes that are necessary to
|
|
22
|
+
* apply to `existing` are returned.
|
|
23
|
+
*
|
|
24
|
+
* If `ignore` is provided, any changes that were made to a path that
|
|
25
|
+
* starts with any of the `ignore` paths are skipped.
|
|
26
|
+
*/ export const getChanges = ({ from: source, to: target, existing, ignore })=>{
|
|
27
|
+
const changes = new ChangesWithHash();
|
|
28
|
+
const targetPaths = Object.keys(target);
|
|
29
|
+
for (const [sourcePath, sourceHash] of Object.entries(source)){
|
|
30
|
+
if (ignore?.some((ignored)=>sourcePath.startsWith(ignored))) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const targetHash = target[sourcePath];
|
|
34
|
+
if (!targetHash) {
|
|
35
|
+
if (!sourcePath.endsWith("/") || !targetPaths.some((targetPath)=>targetPath.startsWith(sourcePath))) {
|
|
36
|
+
// sourcePath is a file and it doesn't exist in target OR
|
|
37
|
+
// sourcePath is a directory and target doesn't have any
|
|
38
|
+
// existing files inside it, therefor the sourcePath has been
|
|
39
|
+
// deleted
|
|
40
|
+
changes.set(sourcePath, {
|
|
41
|
+
type: "delete",
|
|
42
|
+
sourceHash
|
|
43
|
+
});
|
|
44
|
+
log.trace("file deleted", {
|
|
45
|
+
path: sourcePath,
|
|
46
|
+
sourceHash
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else if (!isEqualHash(sourceHash, targetHash)) {
|
|
50
|
+
// the file or directory exists in target, but has a different
|
|
51
|
+
// hash, so it's been updated
|
|
52
|
+
changes.set(sourcePath, {
|
|
53
|
+
type: "update",
|
|
54
|
+
sourceHash,
|
|
55
|
+
targetHash
|
|
56
|
+
});
|
|
57
|
+
log.trace("file updated", {
|
|
58
|
+
path: sourcePath,
|
|
59
|
+
sourceHash,
|
|
60
|
+
targetHash
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const targetPath of targetPaths){
|
|
65
|
+
if (ignore?.some((ignored)=>targetPath.startsWith(ignored))) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!source[targetPath]) {
|
|
69
|
+
// the targetPath doesn't exist in source, so it's been created
|
|
70
|
+
const targetHash = target[targetPath];
|
|
71
|
+
assert(targetHash, "targetHash should exist");
|
|
72
|
+
changes.set(targetPath, {
|
|
73
|
+
type: "create",
|
|
74
|
+
targetHash
|
|
75
|
+
});
|
|
76
|
+
log.trace("file created", {
|
|
77
|
+
path: targetPath,
|
|
78
|
+
targetHash
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!existing) {
|
|
83
|
+
return changes;
|
|
84
|
+
}
|
|
85
|
+
return withoutUnnecessaryChanges({
|
|
86
|
+
changes,
|
|
87
|
+
existing
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Filters out changes that the existing filesystem already has.
|
|
92
|
+
*/ export const withoutUnnecessaryChanges = ({ changes, existing })=>{
|
|
93
|
+
const necessaryChanges = new ChangesWithHash();
|
|
94
|
+
for (const [path, change] of changes){
|
|
95
|
+
const existingHash = existing[path];
|
|
96
|
+
if (change.type === "delete" && !existingHash) {
|
|
97
|
+
// already deleted
|
|
98
|
+
log.trace("already deleted", {
|
|
99
|
+
path
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (change.type !== "delete" && existingHash && isEqualHash(change.targetHash, existingHash)) {
|
|
104
|
+
// already created or updated
|
|
105
|
+
log.trace("already created or updated", {
|
|
106
|
+
path,
|
|
107
|
+
existingHash,
|
|
108
|
+
targetHash: change.targetHash
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// we could do this:
|
|
113
|
+
// if (change.type === "update" && !existingHash) {
|
|
114
|
+
// change = { type: "create", targetHash: change.targetHash };
|
|
115
|
+
// }
|
|
116
|
+
// but, changing the type makes the output look confusing and it
|
|
117
|
+
// doesn't change the outcome, so we just leave it as is
|
|
118
|
+
necessaryChanges.set(path, change);
|
|
119
|
+
}
|
|
120
|
+
return necessaryChanges;
|
|
121
|
+
};
|
|
122
|
+
export const isEqualHash = (a, b)=>{
|
|
123
|
+
return a.sha1 === b.sha1 && (isNil(a.permissions) || isNil(b.permissions) || a.permissions === b.permissions);
|
|
124
|
+
};
|
|
125
|
+
export const isEqualHashes = (a, b)=>{
|
|
126
|
+
for (const [aPath, aHash] of Object.entries(a)){
|
|
127
|
+
const bHash = b[aPath];
|
|
128
|
+
if (!bHash || !isEqualHash(aHash, bHash)) {
|
|
129
|
+
log.trace("hashes are not equal", {
|
|
130
|
+
path: aPath,
|
|
131
|
+
aHash,
|
|
132
|
+
bHash
|
|
133
|
+
});
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const bPath of Object.keys(b)){
|
|
138
|
+
if (!a[bPath]) {
|
|
139
|
+
log.trace("hashes are not equal", {
|
|
140
|
+
path: bPath,
|
|
141
|
+
aHash: undefined,
|
|
142
|
+
bHash: b[bPath]
|
|
143
|
+
});
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=hashes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/services/filesync/hashes.ts"],"sourcesContent":["import assert from \"node:assert\";\nimport { createLogger } from \"../output/log/logger.js\";\nimport { isNil } from \"../util/is.js\";\nimport { type Create, type Delete, type Update } from \"./changes.js\";\nimport type { Hash, Hashes } from \"./directory.js\";\n\nconst log = createLogger({ name: \"hashes\" });\n\nexport type CreateWithHash = Create & { targetHash: Hash };\nexport type UpdateWithHash = Update & { sourceHash: Hash; targetHash: Hash };\nexport type DeleteWithHash = Delete & { sourceHash: Hash };\nexport type ChangeWithHash = CreateWithHash | UpdateWithHash | DeleteWithHash;\n\nexport class ChangesWithHash extends Map<string, ChangeWithHash> {\n created(): string[] {\n return Array.from(this.entries())\n .filter(([, change]) => change.type === \"create\")\n .map(([path]) => path);\n }\n\n updated(): string[] {\n return Array.from(this.entries())\n .filter(([, change]) => change.type === \"update\")\n .map(([path]) => path);\n }\n\n deleted(): string[] {\n return Array.from(this.entries())\n .filter(([, change]) => change.type === \"delete\")\n .map(([path]) => path);\n }\n}\n\n/**\n * Calculates the changes that were made to `from` to make it end up as `to`.\n *\n * If `existing` is provided, only the changes that are necessary to\n * apply to `existing` are returned.\n *\n * If `ignore` is provided, any changes that were made to a path that\n * starts with any of the `ignore` paths are skipped.\n */\nexport const getChanges = ({\n from: source,\n to: target,\n existing,\n ignore,\n}: {\n from: Hashes;\n to: Hashes;\n existing?: Hashes;\n ignore?: string[];\n}): ChangesWithHash => {\n const changes = new ChangesWithHash();\n const targetPaths = Object.keys(target);\n\n for (const [sourcePath, sourceHash] of Object.entries(source)) {\n if (ignore?.some((ignored) => sourcePath.startsWith(ignored))) {\n continue;\n }\n\n const targetHash = target[sourcePath];\n if (!targetHash) {\n if (!sourcePath.endsWith(\"/\") || !targetPaths.some((targetPath) => targetPath.startsWith(sourcePath))) {\n // sourcePath is a file and it doesn't exist in target OR\n // sourcePath is a directory and target doesn't have any\n // existing files inside it, therefor the sourcePath has been\n // deleted\n changes.set(sourcePath, { type: \"delete\", sourceHash });\n log.trace(\"file deleted\", { path: sourcePath, sourceHash });\n }\n } else if (!isEqualHash(sourceHash, targetHash)) {\n // the file or directory exists in target, but has a different\n // hash, so it's been updated\n changes.set(sourcePath, { type: \"update\", sourceHash, targetHash });\n log.trace(\"file updated\", { path: sourcePath, sourceHash, targetHash });\n }\n }\n\n for (const targetPath of targetPaths) {\n if (ignore?.some((ignored) => targetPath.startsWith(ignored))) {\n continue;\n }\n\n if (!source[targetPath]) {\n // the targetPath doesn't exist in source, so it's been created\n const targetHash = target[targetPath];\n assert(targetHash, \"targetHash should exist\");\n\n changes.set(targetPath, { type: \"create\", targetHash });\n log.trace(\"file created\", { path: targetPath, targetHash });\n }\n }\n\n if (!existing) {\n return changes;\n }\n\n return withoutUnnecessaryChanges({ changes, existing });\n};\n\n/**\n * Filters out changes that the existing filesystem already has.\n */\nexport const withoutUnnecessaryChanges = ({ changes, existing }: { changes: ChangesWithHash; existing: Hashes }): ChangesWithHash => {\n const necessaryChanges = new ChangesWithHash();\n\n for (const [path, change] of changes) {\n const existingHash = existing[path];\n if (change.type === \"delete\" && !existingHash) {\n // already deleted\n log.trace(\"already deleted\", { path });\n continue;\n }\n\n if (change.type !== \"delete\" && existingHash && isEqualHash(change.targetHash, existingHash)) {\n // already created or updated\n log.trace(\"already created or updated\", { path, existingHash, targetHash: change.targetHash });\n continue;\n }\n\n // we could do this:\n // if (change.type === \"update\" && !existingHash) {\n // change = { type: \"create\", targetHash: change.targetHash };\n // }\n // but, changing the type makes the output look confusing and it\n // doesn't change the outcome, so we just leave it as is\n\n necessaryChanges.set(path, change);\n }\n\n return necessaryChanges;\n};\n\nexport const isEqualHash = (a: Hash, b: Hash): boolean => {\n return a.sha1 === b.sha1 && (isNil(a.permissions) || isNil(b.permissions) || a.permissions === b.permissions);\n};\n\nexport const isEqualHashes = (a: Hashes, b: Hashes): boolean => {\n for (const [aPath, aHash] of Object.entries(a)) {\n const bHash = b[aPath];\n if (!bHash || !isEqualHash(aHash, bHash)) {\n log.trace(\"hashes are not equal\", { path: aPath, aHash, bHash });\n return false;\n }\n }\n\n for (const bPath of Object.keys(b)) {\n if (!a[bPath]) {\n log.trace(\"hashes are not equal\", { path: bPath, aHash: undefined, bHash: b[bPath] });\n return false;\n }\n }\n\n return true;\n};\n"],"names":["assert","createLogger","isNil","log","name","ChangesWithHash","Map","created","Array","from","entries","filter","change","type","map","path","updated","deleted","getChanges","source","to","target","existing","ignore","changes","targetPaths","Object","keys","sourcePath","sourceHash","some","ignored","startsWith","targetHash","endsWith","targetPath","set","trace","isEqualHash","withoutUnnecessaryChanges","necessaryChanges","existingHash","a","b","sha1","permissions","isEqualHashes","aPath","aHash","bHash","bPath","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HTTPError } from "got";
|
|
2
|
+
import { config } from "../config/config.js";
|
|
3
|
+
import { readSession } from "../user/session.js";
|
|
4
|
+
import { log } from "./http.js";
|
|
5
|
+
/**
|
|
6
|
+
* Determines whether the given request options are for a Gadget
|
|
7
|
+
* Services request.
|
|
8
|
+
*
|
|
9
|
+
* @param options - The request options to check.
|
|
10
|
+
* @returns True if the request options are for a Gadget Services
|
|
11
|
+
* request, false otherwise.
|
|
12
|
+
*/ export const isGadgetServicesRequest = (options)=>{
|
|
13
|
+
return options.url instanceof URL && options.url.host === config.domains.services;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Loads the cookie from the session.
|
|
17
|
+
*
|
|
18
|
+
* @returns The cookie string or undefined if there is no session.
|
|
19
|
+
*/ export const loadCookie = ()=>{
|
|
20
|
+
const token = readSession();
|
|
21
|
+
return token && `session=${encodeURIComponent(token)};`;
|
|
22
|
+
};
|
|
23
|
+
export const isUnauthorizedError = (error)=>{
|
|
24
|
+
return error instanceof HTTPError && error.response.statusCode === 401;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Swallows unauthorized errors and logs a warning, rethrows all other
|
|
28
|
+
* errors.
|
|
29
|
+
*
|
|
30
|
+
* @param error The error to handle.
|
|
31
|
+
*/ export const swallowUnauthorized = (error)=>{
|
|
32
|
+
if (isUnauthorizedError(error)) {
|
|
33
|
+
log.warn("swallowing unauthorized error", {
|
|
34
|
+
error
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/services/http/auth.ts"],"sourcesContent":["import { HTTPError, type OptionsInit } from \"got\";\nimport { config } from \"../config/config.js\";\nimport { readSession } from \"../user/session.js\";\nimport { log } from \"./http.js\";\n\n/**\n * Determines whether the given request options are for a Gadget\n * Services request.\n *\n * @param options - The request options to check.\n * @returns True if the request options are for a Gadget Services\n * request, false otherwise.\n */\nexport const isGadgetServicesRequest = (options: OptionsInit): boolean => {\n return options.url instanceof URL && options.url.host === config.domains.services;\n};\n\n/**\n * Loads the cookie from the session.\n *\n * @returns The cookie string or undefined if there is no session.\n */\n\nexport const loadCookie = (): string | undefined => {\n const token = readSession();\n return token && `session=${encodeURIComponent(token)};`;\n};\n\nexport const isUnauthorizedError = (error: unknown): error is HTTPError => {\n return error instanceof HTTPError && error.response.statusCode === 401;\n};\n\n/**\n * Swallows unauthorized errors and logs a warning, rethrows all other\n * errors.\n *\n * @param error The error to handle.\n */\n\nexport const swallowUnauthorized = (error: unknown): void => {\n if (isUnauthorizedError(error)) {\n log.warn(\"swallowing unauthorized error\", { error });\n return;\n }\n throw error;\n};\n"],"names":["HTTPError","config","readSession","log","isGadgetServicesRequest","options","url","URL","host","domains","services","loadCookie","token","encodeURIComponent","isUnauthorizedError","error","response","statusCode","swallowUnauthorized","warn"],"mappings":"AAAA,SAASA,SAAS,QAA0B,MAAM;AAClD,SAASC,MAAM,QAAQ,sBAAsB;AAC7C,SAASC,WAAW,QAAQ,qBAAqB;AACjD,SAASC,GAAG,QAAQ,YAAY;AAEhC;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,CAACC;IACtC,OAAOA,QAAQC,GAAG,YAAYC,OAAOF,QAAQC,GAAG,CAACE,IAAI,KAAKP,OAAOQ,OAAO,CAACC,QAAQ;AACnF,EAAE;AAEF;;;;CAIC,GAED,OAAO,MAAMC,aAAa;IACxB,MAAMC,QAAQV;IACd,OAAOU,SAAS,CAAC,QAAQ,EAAEC,mBAAmBD,OAAO,CAAC,CAAC;AACzD,EAAE;AAEF,OAAO,MAAME,sBAAsB,CAACC;IAClC,OAAOA,iBAAiBf,aAAae,MAAMC,QAAQ,CAACC,UAAU,KAAK;AACrE,EAAE;AAEF;;;;;CAKC,GAED,OAAO,MAAMC,sBAAsB,CAACH;IAClC,IAAID,oBAAoBC,QAAQ;QAC9BZ,IAAIgB,IAAI,CAAC,iCAAiC;YAAEJ;QAAM;QAClD;IACF;IACA,MAAMA;AACR,EAAE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { got } from "got";
|
|
2
|
+
import { config } from "../config/config.js";
|
|
3
|
+
import { createLogger } from "../output/log/logger.js";
|
|
4
|
+
import { writeSession } from "../user/session.js";
|
|
5
|
+
import { isGadgetServicesRequest } from "./auth.js";
|
|
6
|
+
export const log = createLogger({
|
|
7
|
+
name: "http"
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* An instance of the `got` library with hooks for logging and handling
|
|
11
|
+
* 401 errors. This should be used for all HTTP requests.
|
|
12
|
+
*/ export const http = got.extend({
|
|
13
|
+
hooks: {
|
|
14
|
+
beforeRequest: [
|
|
15
|
+
(options)=>{
|
|
16
|
+
options.headers["user-agent"] = config.versionFull;
|
|
17
|
+
log.debug("http request", {
|
|
18
|
+
request: {
|
|
19
|
+
method: options.method,
|
|
20
|
+
url: options.url?.toString()
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
beforeRetry: [
|
|
26
|
+
(error, retryCount)=>{
|
|
27
|
+
log.warn("http request failed, retrying...", {
|
|
28
|
+
retryCount,
|
|
29
|
+
error: {
|
|
30
|
+
code: error.code,
|
|
31
|
+
name: error.name,
|
|
32
|
+
message: error.message
|
|
33
|
+
},
|
|
34
|
+
request: error.request && {
|
|
35
|
+
method: error.request.options.method,
|
|
36
|
+
url: error.request.options.url?.toString()
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
afterResponse: [
|
|
42
|
+
(response)=>{
|
|
43
|
+
log.debug("http response", {
|
|
44
|
+
request: {
|
|
45
|
+
method: response.request.options.method,
|
|
46
|
+
url: response.request.options.url?.toString()
|
|
47
|
+
},
|
|
48
|
+
response: {
|
|
49
|
+
statusCode: response.statusCode,
|
|
50
|
+
traceId: response.headers["x-trace-id"],
|
|
51
|
+
durationMs: response.timings.phases.total
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (response.statusCode === 401 && isGadgetServicesRequest(response.request.options)) {
|
|
55
|
+
// clear the session if the request was unauthorized
|
|
56
|
+
writeSession(undefined);
|
|
57
|
+
}
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/services/http/http.ts"],"sourcesContent":["import { got } from \"got\";\nimport { config } from \"../config/config.js\";\nimport { createLogger } from \"../output/log/logger.js\";\nimport { writeSession } from \"../user/session.js\";\nimport { isGadgetServicesRequest } from \"./auth.js\";\n\nexport const log = createLogger({ name: \"http\" });\n\n/**\n * An instance of the `got` library with hooks for logging and handling\n * 401 errors. This should be used for all HTTP requests.\n */\nexport const http = got.extend({\n hooks: {\n beforeRequest: [\n (options) => {\n options.headers[\"user-agent\"] = config.versionFull;\n log.debug(\"http request\", {\n request: {\n method: options.method,\n url: options.url?.toString(),\n },\n });\n },\n ],\n beforeRetry: [\n (error, retryCount) => {\n log.warn(\"http request failed, retrying...\", {\n retryCount,\n error: {\n code: error.code,\n name: error.name,\n message: error.message,\n },\n request: error.request && {\n method: error.request.options.method,\n url: error.request.options.url?.toString(),\n },\n });\n },\n ],\n afterResponse: [\n (response) => {\n log.debug(\"http response\", {\n request: {\n method: response.request.options.method,\n url: response.request.options.url?.toString(),\n },\n response: {\n statusCode: response.statusCode,\n traceId: response.headers[\"x-trace-id\"],\n durationMs: response.timings.phases.total,\n },\n });\n\n if (response.statusCode === 401 && isGadgetServicesRequest(response.request.options)) {\n // clear the session if the request was unauthorized\n writeSession(undefined);\n }\n\n return response;\n },\n ],\n },\n});\n"],"names":["got","config","createLogger","writeSession","isGadgetServicesRequest","log","name","http","extend","hooks","beforeRequest","options","headers","versionFull","debug","request","method","url","toString","beforeRetry","error","retryCount","warn","code","message","afterResponse","response","statusCode","traceId","durationMs","timings","phases","total","undefined"],"mappings":"AAAA,SAASA,GAAG,QAAQ,MAAM;AAC1B,SAASC,MAAM,QAAQ,sBAAsB;AAC7C,SAASC,YAAY,QAAQ,0BAA0B;AACvD,SAASC,YAAY,QAAQ,qBAAqB;AAClD,SAASC,uBAAuB,QAAQ,YAAY;AAEpD,OAAO,MAAMC,MAAMH,aAAa;IAAEI,MAAM;AAAO,GAAG;AAElD;;;CAGC,GACD,OAAO,MAAMC,OAAOP,IAAIQ,MAAM,CAAC;IAC7BC,OAAO;QACLC,eAAe;YACb,CAACC;gBACCA,QAAQC,OAAO,CAAC,aAAa,GAAGX,OAAOY,WAAW;gBAClDR,IAAIS,KAAK,CAAC,gBAAgB;oBACxBC,SAAS;wBACPC,QAAQL,QAAQK,MAAM;wBACtBC,KAAKN,QAAQM,GAAG,EAAEC;oBACpB;gBACF;YACF;SACD;QACDC,aAAa;YACX,CAACC,OAAOC;gBACNhB,IAAIiB,IAAI,CAAC,oCAAoC;oBAC3CD;oBACAD,OAAO;wBACLG,MAAMH,MAAMG,IAAI;wBAChBjB,MAAMc,MAAMd,IAAI;wBAChBkB,SAASJ,MAAMI,OAAO;oBACxB;oBACAT,SAASK,MAAML,OAAO,IAAI;wBACxBC,QAAQI,MAAML,OAAO,CAACJ,OAAO,CAACK,MAAM;wBACpCC,KAAKG,MAAML,OAAO,CAACJ,OAAO,CAACM,GAAG,EAAEC;oBAClC;gBACF;YACF;SACD;QACDO,eAAe;YACb,CAACC;gBACCrB,IAAIS,KAAK,CAAC,iBAAiB;oBACzBC,SAAS;wBACPC,QAAQU,SAASX,OAAO,CAACJ,OAAO,CAACK,MAAM;wBACvCC,KAAKS,SAASX,OAAO,CAACJ,OAAO,CAACM,GAAG,EAAEC;oBACrC;oBACAQ,UAAU;wBACRC,YAAYD,SAASC,UAAU;wBAC/BC,SAASF,SAASd,OAAO,CAAC,aAAa;wBACvCiB,YAAYH,SAASI,OAAO,CAACC,MAAM,CAACC,KAAK;oBAC3C;gBACF;gBAEA,IAAIN,SAASC,UAAU,KAAK,OAAOvB,wBAAwBsB,SAASX,OAAO,CAACJ,OAAO,GAAG;oBACpF,oDAAoD;oBACpDR,aAAa8B;gBACf;gBAEA,OAAOP;YACT;SACD;IACH;AACF,GAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/services/output/log/field.ts"],"sourcesContent":["import { type JsonObject, type JsonPrimitive } from \"type-fest\";\n\nexport type Fields = Record<string, Field> | { error: unknown } | { reason: unknown };\n\nexport type Field = FieldPrimitive | FieldObject | FieldArray;\n\nexport type FieldPrimitive = JsonPrimitive | bigint | undefined;\n\nexport type FieldObject = JsonObject | { [key in string]: Field } | Map<FieldPrimitive, Field>;\n\nexport type FieldArray = Field[] | Set<Field>;\n"],"names":[],"mappings":"AAUA,WAA8C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/services/output/log/format/format.ts"],"sourcesContent":["import type { Fields } from \"../field.js\";\nimport type { Level } from \"../level.js\";\nimport { formatJson } from \"./json.js\";\nimport { formatPretty } from \"./pretty.js\";\n\nexport type Formatter = (level: Level, name: string, msg: string, fields: Fields) => string;\n\nexport const formatters = {\n pretty: formatPretty,\n json: formatJson,\n} as const;\n"],"names":["formatJson","formatPretty","formatters","pretty","json"],"mappings":"AAEA,SAASA,UAAU,QAAQ,YAAY;AACvC,SAASC,YAAY,QAAQ,cAAc;AAI3C,OAAO,MAAMC,aAAa;IACxBC,QAAQF;IACRG,MAAMJ;AACR,EAAW"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import stripAnsi from "strip-ansi";
|
|
3
|
+
import { config } from "../../../config/config.js";
|
|
4
|
+
import { isObject, isString } from "../../../util/is.js";
|
|
5
|
+
import { Level } from "../level.js";
|
|
6
|
+
export const formatJson = (level, name, msg, fields)=>{
|
|
7
|
+
return JSON.stringify({
|
|
8
|
+
level,
|
|
9
|
+
name,
|
|
10
|
+
msg: stripAnsi(msg).trim(),
|
|
11
|
+
fields: serializeFields(fields)
|
|
12
|
+
}) + "\n";
|
|
13
|
+
};
|
|
14
|
+
const serializeFields = (fields)=>{
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const [key, value] of Object.entries(fields)){
|
|
17
|
+
result[key] = serializeValue(value);
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
const serializeValue = (value)=>{
|
|
22
|
+
if (value instanceof Set) {
|
|
23
|
+
value = Array.from(value);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (value.length > 10 && config.logLevel > Level.TRACE) {
|
|
27
|
+
// truncate arrays to 10 elements when not tracing
|
|
28
|
+
value = value.slice(0, 10);
|
|
29
|
+
assert(Array.isArray(value));
|
|
30
|
+
}
|
|
31
|
+
return value.map(serializeValue);
|
|
32
|
+
}
|
|
33
|
+
if (value instanceof Map) {
|
|
34
|
+
value = Object.fromEntries(value.entries());
|
|
35
|
+
}
|
|
36
|
+
if (isObject(value)) {
|
|
37
|
+
return serializeFields(value);
|
|
38
|
+
}
|
|
39
|
+
if (isString(value)) {
|
|
40
|
+
return stripAnsi(value).trim();
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/services/output/log/format/json.ts"],"sourcesContent":["import assert from \"node:assert\";\nimport stripAnsi from \"strip-ansi\";\nimport { config } from \"../../../config/config.js\";\nimport { isObject, isString } from \"../../../util/is.js\";\nimport { Level } from \"../level.js\";\nimport type { Formatter } from \"./format.js\";\n\nexport const formatJson: Formatter = (level, name, msg, fields) => {\n return JSON.stringify({ level, name, msg: stripAnsi(msg).trim(), fields: serializeFields(fields) }) + \"\\n\";\n};\n\nconst serializeFields = (fields: Record<string, unknown>): Record<string, unknown> => {\n const result = {} as Record<string, unknown>;\n for (const [key, value] of Object.entries(fields)) {\n result[key] = serializeValue(value);\n }\n return result;\n};\n\nconst serializeValue = (value: unknown): unknown => {\n if (value instanceof Set) {\n value = Array.from(value);\n }\n\n if (Array.isArray(value)) {\n if (value.length > 10 && config.logLevel > Level.TRACE) {\n // truncate arrays to 10 elements when not tracing\n value = value.slice(0, 10);\n assert(Array.isArray(value));\n }\n return value.map(serializeValue);\n }\n\n if (value instanceof Map) {\n value = Object.fromEntries(value.entries());\n }\n\n if (isObject(value)) {\n return serializeFields(value as Record<string, unknown>);\n }\n\n if (isString(value)) {\n return stripAnsi(value).trim();\n }\n\n return value;\n};\n"],"names":["assert","stripAnsi","config","isObject","isString","Level","formatJson","level","name","msg","fields","JSON","stringify","trim","serializeFields","result","key","value","Object","entries","serializeValue","Set","Array","from","isArray","length","logLevel","TRACE","slice","map","Map","fromEntries"],"mappings":"AAAA,OAAOA,YAAY,cAAc;AACjC,OAAOC,eAAe,aAAa;AACnC,SAASC,MAAM,QAAQ,4BAA4B;AACnD,SAASC,QAAQ,EAAEC,QAAQ,QAAQ,sBAAsB;AACzD,SAASC,KAAK,QAAQ,cAAc;AAGpC,OAAO,MAAMC,aAAwB,CAACC,OAAOC,MAAMC,KAAKC;IACtD,OAAOC,KAAKC,SAAS,CAAC;QAAEL;QAAOC;QAAMC,KAAKR,UAAUQ,KAAKI,IAAI;QAAIH,QAAQI,gBAAgBJ;IAAQ,KAAK;AACxG,EAAE;AAEF,MAAMI,kBAAkB,CAACJ;IACvB,MAAMK,SAAS,CAAC;IAChB,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACT,QAAS;QACjDK,MAAM,CAACC,IAAI,GAAGI,eAAeH;IAC/B;IACA,OAAOF;AACT;AAEA,MAAMK,iBAAiB,CAACH;IACtB,IAAIA,iBAAiBI,KAAK;QACxBJ,QAAQK,MAAMC,IAAI,CAACN;IACrB;IAEA,IAAIK,MAAME,OAAO,CAACP,QAAQ;QACxB,IAAIA,MAAMQ,MAAM,GAAG,MAAMvB,OAAOwB,QAAQ,GAAGrB,MAAMsB,KAAK,EAAE;YACtD,kDAAkD;YAClDV,QAAQA,MAAMW,KAAK,CAAC,GAAG;YACvB5B,OAAOsB,MAAME,OAAO,CAACP;QACvB;QACA,OAAOA,MAAMY,GAAG,CAACT;IACnB;IAEA,IAAIH,iBAAiBa,KAAK;QACxBb,QAAQC,OAAOa,WAAW,CAACd,MAAME,OAAO;IAC1C;IAEA,IAAIhB,SAASc,QAAQ;QACnB,OAAOH,gBAAgBG;IACzB;IAEA,IAAIb,SAASa,QAAQ;QACnB,OAAOhB,UAAUgB,OAAOJ,IAAI;IAC9B;IAEA,OAAOI;AACT"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import chalk, { Chalk } from "chalk";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import { config } from "../../../config/config.js";
|
|
5
|
+
import { env } from "../../../config/env.js";
|
|
6
|
+
import { isObject } from "../../../util/is.js";
|
|
7
|
+
import { Level } from "../level.js";
|
|
8
|
+
export const formatPretty = (level, name, msg, fields)=>{
|
|
9
|
+
return `${formatTimestamp()} ${formatLevel(level)} ${formatName(name)}:${formatMessage(msg)}${formatFields(fields)}${NEW_LINE}`;
|
|
10
|
+
};
|
|
11
|
+
const color = new Chalk({
|
|
12
|
+
// we always turn off colors in tests (FORCE_COLOR=0) so that we get
|
|
13
|
+
// predictable output, but if we're running with logs enabled
|
|
14
|
+
// (GGT_LOG_LEVEL=info), we still want to see colors in our logs
|
|
15
|
+
level: env.testLike && config.logLevel < Level.PRINT ? 3 : chalk.level
|
|
16
|
+
});
|
|
17
|
+
const blue = color.hex("#86B5F7");
|
|
18
|
+
const blueLight = color.hex("#B2D0FA");
|
|
19
|
+
const gray = color.hex("#D6D6D6");
|
|
20
|
+
const grayDark = color.hex("#C2C2C2");
|
|
21
|
+
const green = color.hex("#9DE6A4");
|
|
22
|
+
const greenLight = color.hex("#BEEEC3");
|
|
23
|
+
const orange = color.hex("#EEAC78");
|
|
24
|
+
const orangeLight = color.hex("#F4C7A4");
|
|
25
|
+
const pink = color.hex("#FAACB5");
|
|
26
|
+
const red = color.hex("#A64E4E");
|
|
27
|
+
const white = color.hex("#FFFFFF");
|
|
28
|
+
const EMPTY = "";
|
|
29
|
+
const SPACE = " ";
|
|
30
|
+
const NEW_LINE = "\n";
|
|
31
|
+
const COLON = ":";
|
|
32
|
+
const QUOTE = "'";
|
|
33
|
+
const formatKey = (key, indent)=>{
|
|
34
|
+
const color = key === "error" ? red : gray;
|
|
35
|
+
const buf = [];
|
|
36
|
+
buf.push(NEW_LINE);
|
|
37
|
+
for(let i = 0; i < indent; i++){
|
|
38
|
+
buf.push(SPACE);
|
|
39
|
+
}
|
|
40
|
+
buf.push(color(key));
|
|
41
|
+
buf.push(COLON);
|
|
42
|
+
return buf.join("");
|
|
43
|
+
};
|
|
44
|
+
const formatValue = (value, color, indent)=>{
|
|
45
|
+
const lines = value.split(NEW_LINE);
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
return EMPTY;
|
|
48
|
+
}
|
|
49
|
+
const buf = [];
|
|
50
|
+
const firstLine = lines.shift();
|
|
51
|
+
assert(firstLine);
|
|
52
|
+
buf.push(color(firstLine));
|
|
53
|
+
// color the rest of the lines
|
|
54
|
+
for (const line of lines){
|
|
55
|
+
if (!line) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
buf.push(NEW_LINE);
|
|
59
|
+
for(let i = 0; i < indent; i++){
|
|
60
|
+
buf.push(SPACE);
|
|
61
|
+
}
|
|
62
|
+
buf.push(color(line));
|
|
63
|
+
}
|
|
64
|
+
return buf.join(EMPTY);
|
|
65
|
+
};
|
|
66
|
+
const formatFields = (fields, indent = 2)=>{
|
|
67
|
+
if (Object.keys(fields).length === 0) {
|
|
68
|
+
return EMPTY;
|
|
69
|
+
}
|
|
70
|
+
const buf = [];
|
|
71
|
+
for (let [key, value] of Object.entries(fields)){
|
|
72
|
+
buf.push(formatKey(key, indent));
|
|
73
|
+
if (value instanceof Set) {
|
|
74
|
+
value = Array.from(value);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
if (value.length === 0) {
|
|
78
|
+
buf.push(formatValue(" []", gray, indent));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (value.length > 10 && config.logLevel > Level.TRACE) {
|
|
82
|
+
// truncate arrays to 10 elements when not tracing
|
|
83
|
+
value = value.slice(0, 10);
|
|
84
|
+
assert(Array.isArray(value));
|
|
85
|
+
}
|
|
86
|
+
value = Object.fromEntries(value.entries());
|
|
87
|
+
}
|
|
88
|
+
if (value instanceof Map) {
|
|
89
|
+
value = Object.fromEntries(value.entries());
|
|
90
|
+
}
|
|
91
|
+
if (isObject(value)) {
|
|
92
|
+
buf.push(formatFields(value, indent + 2));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
buf.push(SPACE);
|
|
96
|
+
switch(typeof value){
|
|
97
|
+
case "string":
|
|
98
|
+
buf.push(formatValue(QUOTE + value.replaceAll(NEW_LINE, NEW_LINE + SPACE.repeat(indent + key.length)) + QUOTE, blueLight, indent));
|
|
99
|
+
break;
|
|
100
|
+
case "number":
|
|
101
|
+
buf.push(formatValue(String(value), orangeLight, indent));
|
|
102
|
+
break;
|
|
103
|
+
case "bigint":
|
|
104
|
+
buf.push(formatValue(String(value) + "n", orangeLight, indent));
|
|
105
|
+
break;
|
|
106
|
+
case "boolean":
|
|
107
|
+
buf.push(formatValue(String(value), greenLight, indent));
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
buf.push(formatValue(String(value), white, indent));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return buf.join(EMPTY);
|
|
115
|
+
};
|
|
116
|
+
const formatTimestamp = ()=>{
|
|
117
|
+
const ts = dayjs().format("hh:mm:ss");
|
|
118
|
+
return grayDark(ts);
|
|
119
|
+
};
|
|
120
|
+
const formatLevel = (level)=>{
|
|
121
|
+
switch(level){
|
|
122
|
+
case Level.PRINT:
|
|
123
|
+
return gray("PRINT");
|
|
124
|
+
case Level.TRACE:
|
|
125
|
+
return blue("TRACE");
|
|
126
|
+
case Level.DEBUG:
|
|
127
|
+
return orange("DEBUG");
|
|
128
|
+
case Level.INFO:
|
|
129
|
+
return green("INFO");
|
|
130
|
+
case Level.WARN:
|
|
131
|
+
return pink("WARN");
|
|
132
|
+
case Level.ERROR:
|
|
133
|
+
return red("ERROR");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const formatName = (name)=>{
|
|
137
|
+
return white(name);
|
|
138
|
+
};
|
|
139
|
+
const formatMessage = (msg)=>{
|
|
140
|
+
const lines = msg.split(NEW_LINE);
|
|
141
|
+
if (lines.length === 1) {
|
|
142
|
+
return SPACE + white(msg);
|
|
143
|
+
}
|
|
144
|
+
return NEW_LINE + lines.map((line)=>SPACE + SPACE + line).join(NEW_LINE);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=pretty.js.map
|