@bubblydoo/uxp-toolkit 0.0.2

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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/index.d.ts +271 -0
  4. package/dist/index.js +733 -0
  5. package/package.json +41 -0
  6. package/src/commands-library/getLayerProperties.ts +32 -0
  7. package/src/commands-library/renameLayer.ts +15 -0
  8. package/src/commands-library/renameLayer.uxp-test.ts +32 -0
  9. package/src/core/batchPlay.ts +16 -0
  10. package/src/core/command.ts +130 -0
  11. package/src/core/executeAsModal.ts +101 -0
  12. package/src/core/suspendHistory.ts +15 -0
  13. package/src/core/suspendHistory.uxp-test.ts +18 -0
  14. package/src/core-wrappers/executeAsModalAndSuspendHistory.ts +11 -0
  15. package/src/dom/getFlattenedDomLayersList.ts +43 -0
  16. package/src/dom/photoshopDomLayersToTree.ts +18 -0
  17. package/src/error-sourcemaps/sourcemaps.ts +100 -0
  18. package/src/error-sourcemaps/sourcemaps.uxp-test.ts +24 -0
  19. package/src/errors/ut-error.ts +6 -0
  20. package/src/filesystem/openFileByPath.ts +10 -0
  21. package/src/general-tree/flattenTree.ts +12 -0
  22. package/src/general-tree/layerRef.ts +4 -0
  23. package/src/general-tree/mapTree.ts +11 -0
  24. package/src/general-tree/mapTreeRef.ts +11 -0
  25. package/src/general-tree/treeTypes.ts +7 -0
  26. package/src/index.ts +73 -0
  27. package/src/metadata-storage/metadataStorage.ts +66 -0
  28. package/src/metadata-storage/metadataStorage.uxp-test.ts +35 -0
  29. package/src/node-compat/path/resolvePath.ts +19 -0
  30. package/src/other/applicationInfo.ts +169 -0
  31. package/src/other/applicationInfo.uxp-test.ts +11 -0
  32. package/src/other/clipboard.ts +10 -0
  33. package/src/other/clipboard.uxp-test.ts +17 -0
  34. package/src/other/uxpEntrypoints.ts +9 -0
  35. package/src/ut-tree/getFlattenedLayerDescriptorsList.ts +72 -0
  36. package/src/ut-tree/getLayerEffects.ts +41 -0
  37. package/src/ut-tree/getLayerProperties.ts +35 -0
  38. package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.ts +182 -0
  39. package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test.ts +52 -0
  40. package/src/ut-tree/psLayerRef.ts +4 -0
  41. package/src/ut-tree/utLayersToTree.ts +21 -0
  42. package/src/util/utLayerToLayer.ts +41 -0
  43. package/test/fixtures/clipping-layers.psd +0 -0
  44. package/test/fixtures/one-layer.psd +0 -0
  45. package/test/index.ts +21 -0
  46. package/test/meta-tests/executeAsModal.uxp-test.ts +38 -0
  47. package/test/meta-tests/suspendHistory.uxp-test.ts +27 -0
  48. package/tsconfig.json +13 -0
  49. package/tsup.config.ts +9 -0
  50. package/uxp-tests.json +13 -0
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@bubblydoo/uxp-toolkit",
3
+ "version": "0.0.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./package.json": "./package.json"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "peerDependencies": {
17
+ "zod": "^4.0.0"
18
+ },
19
+ "dependencies": {
20
+ "error-stack-parser": "^2.1.4",
21
+ "source-map-js": "^1.2.1",
22
+ "xmlbuilder2": "^4.0.3"
23
+ },
24
+ "devDependencies": {
25
+ "@adobe-uxp-types/uxp": "^0.0.9",
26
+ "@types/chai": "^5.2.3",
27
+ "@types/node": "^20.8.7",
28
+ "@types/photoshop": "^25.0.2",
29
+ "chai": "^6.2.2",
30
+ "tsup": "^8.5.1",
31
+ "typescript": "^5.8.3",
32
+ "zod": "^4.3.6",
33
+ "@bubblydoo/uxp-test-framework": "0.0.2",
34
+ "@bubblydoo/tsconfig": "0.0.1"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "uxp-tests:build": "create-uxp-test-plugin build",
39
+ "uxp-tests:dev": "create-uxp-test-plugin dev"
40
+ }
41
+ }
@@ -0,0 +1,32 @@
1
+ import type { Document } from "photoshop/dom/Document";
2
+ import { z } from "zod";
3
+ import { batchPlayCommand, createCommand } from "../core/command";
4
+
5
+ // get layer properties like name and layerID for all layers in the document (by index)
6
+ export const getLayerProperties = async (document: Document) => {
7
+ const command = createCommand({
8
+ modifying: false,
9
+ descriptor: {
10
+ _obj: "multiGet",
11
+ _target: { _ref: [{ _ref: "document", _id: document.id }] },
12
+ extendedReference: [
13
+ ["name", "layerID", "visible"],
14
+ { _obj: "layer", index: 1, count: -1 },
15
+ ],
16
+ },
17
+ schema: z.object({
18
+ list: z.array(
19
+ z.object({
20
+ name: z.string(),
21
+ layerID: z.number(),
22
+ visible: z.boolean().optional(),
23
+ })
24
+ )
25
+ })
26
+ });
27
+
28
+ const result = await batchPlayCommand(command);
29
+
30
+ // Reverse to get bottom-up order
31
+ return [...result.list].reverse();
32
+ };
@@ -0,0 +1,15 @@
1
+ import { createCommand } from "../core/command";
2
+ import type { PsLayerRef } from "../ut-tree/psLayerRef";
3
+ import { z } from "zod";
4
+
5
+ export function createRenameLayerCommand(layerRef: PsLayerRef, newName: string) {
6
+ return createCommand({
7
+ modifying: true,
8
+ descriptor: {
9
+ _obj: "set",
10
+ _target: [{ _ref: "layer", _id: layerRef.id }, { _ref: "document", _id: layerRef.docId }],
11
+ to: { _obj: "layer", name: newName },
12
+ },
13
+ schema: z.unknown(),
14
+ });
15
+ }
@@ -0,0 +1,32 @@
1
+ import { executeAsModal } from "../core/executeAsModal";
2
+ import { openFileByPath } from "../filesystem/openFileByPath";
3
+ import { getLayerProperties } from "../ut-tree/getLayerProperties";
4
+ import type { Test } from "@bubblydoo/uxp-test-framework";
5
+ import { expect } from "chai";
6
+ import { app } from "photoshop";
7
+ import { createRenameLayerCommand } from "./renameLayer";
8
+
9
+ async function getFirstLayer() {
10
+ const allLayers = await getLayerProperties(app.activeDocument.id);
11
+ return {
12
+ ref: {
13
+ id: allLayers[0]!.layerID,
14
+ docId: app.activeDocument.id,
15
+ },
16
+ name: allLayers[0]!.name,
17
+ };
18
+ }
19
+
20
+ export const renameLayerTest: Test = {
21
+ name: "Rename Layer",
22
+ async run() {
23
+ await openFileByPath("plugin:/fixtures/one-layer.psd");
24
+ const layer = await getFirstLayer();
25
+ expect(layer.name).to.equal("Layer 1");
26
+ await executeAsModal("Rename Layer", async (ctx) => {
27
+ await ctx.batchPlayCommand(createRenameLayerCommand(layer.ref, "New Name"));
28
+ });
29
+ const layer2 = await getFirstLayer();
30
+ expect(layer2.name).to.equal("New Name");
31
+ },
32
+ };
@@ -0,0 +1,16 @@
1
+ import { action } from "photoshop";
2
+
3
+ type P = Parameters<typeof action.batchPlay>;
4
+
5
+ export type CorrectBatchPlayOptions = P[1] & {
6
+ immediateRedraw?: boolean;
7
+ };
8
+
9
+ export async function batchPlay(actions: P[0], options?: CorrectBatchPlayOptions) {
10
+ return action.batchPlay(actions, {
11
+ ...options,
12
+ modalBehavior: "execute",
13
+ dialogOptions: "silent",
14
+ synchronousExecution: false,
15
+ });
16
+ }
@@ -0,0 +1,130 @@
1
+ import type { ActionDescriptor } from "photoshop/dom/CoreModules";
2
+ import { z } from "zod";
3
+ import { batchPlay } from "./batchPlay";
4
+ import { executeAsModal } from "./executeAsModal";
5
+ import type { CorrectExecutionContext } from "./executeAsModal";
6
+ import { action } from "photoshop";
7
+
8
+ export interface UTCommandBase<T extends any> {
9
+ descriptor: ActionDescriptor;
10
+ schema: z.ZodSchema<T>;
11
+ }
12
+
13
+ export interface UTCommandModifying<T extends any> extends UTCommandBase<T> {
14
+ modifying: true;
15
+ }
16
+
17
+ export interface UTCommandNonModifying<T extends any> extends UTCommandBase<T> {
18
+ modifying: false;
19
+ }
20
+
21
+ export function createCommand<TReturn extends any, TModifying extends boolean>(
22
+ obj: {
23
+ descriptor: ActionDescriptor,
24
+ schema: z.ZodSchema<TReturn>,
25
+ modifying: TModifying,
26
+ }
27
+ ): TModifying extends true ? UTCommandModifying<TReturn> : UTCommandNonModifying<TReturn> {
28
+ return {
29
+ modifying: obj.modifying,
30
+ descriptor: obj.descriptor,
31
+ schema: obj.schema,
32
+ } as any;
33
+ }
34
+
35
+ export type UTCommandResult<C> = C extends UTCommandBase<infer T> ? T : never;
36
+
37
+ type BatchPlayOptions = Parameters<typeof batchPlay>[1];
38
+
39
+ async function batchPlayCommandBase<T extends any>(command: UTCommandBase<T>, options?: BatchPlayOptions) {
40
+ const [result] = await batchPlay([command.descriptor], options);
41
+ if (result?._obj === "error") {
42
+ throw new Error("Batch play command failed", { cause: result });
43
+ }
44
+ return command.schema.parse(result);
45
+ }
46
+
47
+ async function batchPlayCommandsBase<TCommands extends Array<UTCommandBase<any>>>(
48
+ commands: readonly [...TCommands],
49
+ options?: BatchPlayOptions
50
+ ): Promise<{
51
+ [K in keyof TCommands]: UTCommandResult<TCommands[K]>;
52
+ }> {
53
+ const results = await batchPlay(commands.map(command => command.descriptor), options);
54
+ if (results[0]?._obj === "error") {
55
+ throw new Error("Batch play command failed", { cause: results[0] });
56
+ }
57
+ return commands.map((command, index) => command.schema.parse(results[index])) as any;
58
+ }
59
+
60
+ export function batchPlayCommand<T extends any>(command: UTCommandNonModifying<T>, options?: BatchPlayOptions) {
61
+ return batchPlayCommandBase(command, options);
62
+ }
63
+
64
+ export function batchPlayCommands<TCommands extends Array<UTCommandNonModifying<any>>>(commands: readonly [...TCommands], options?: BatchPlayOptions) {
65
+ return batchPlayCommandsBase(commands, options);
66
+ }
67
+
68
+ export function createModifyingBatchPlayContext() {
69
+ return {
70
+ batchPlayCommand: batchPlayCommandBase,
71
+ batchPlayCommands: batchPlayCommandsBase,
72
+ }
73
+ }
74
+ // some examples:
75
+
76
+ // const x = await batchPlay([{
77
+ // _obj: "get",
78
+ // _target: [
79
+ // { _ref: "layer", _id: 1 },
80
+ // ],
81
+ // }])
82
+
83
+ // const command = createCommand({
84
+ // modifying: true,
85
+ // descriptor: {
86
+ // _obj: "get",
87
+ // _target: [
88
+ // { _ref: "layer", _id: 1 },
89
+ // ],
90
+ // },
91
+ // schema: z.unknown()
92
+ // })
93
+
94
+ // const result = await batchPlayCommand(command);
95
+
96
+ // executeAsModal(async (ctx) => {
97
+ // await ctx.batchPlayCommand(command);
98
+ // }, {
99
+ // commandName: "test"
100
+ // })
101
+
102
+ // const x = await batchPlayCommands([
103
+ // {
104
+ // modifying: false,
105
+ // descriptor: {
106
+ // _obj: "get"
107
+ // },
108
+ // schema: z.object({
109
+ // name: z.string(),
110
+ // })
111
+ // },
112
+ // {
113
+ // modifying: false,
114
+ // descriptor: {
115
+ // _obj: "put"
116
+ // },
117
+ // schema: z.object({
118
+ // value: z.string(),
119
+ // })
120
+ // }
121
+ // ])
122
+
123
+ // const y = createCommand({
124
+ // descriptor: {
125
+ // _obj: "get"
126
+ // },
127
+ // schema: z.object({
128
+ // name: z.string(),
129
+ // })
130
+ // })
@@ -0,0 +1,101 @@
1
+ import { core } from "photoshop";
2
+ import { createModifyingBatchPlayContext } from "./command";
3
+
4
+ // copied from devtools:
5
+ // hostControl: {suspendHistory: ƒ, resumeHistory: ƒ, registerAutoCloseDocument: ƒ, unregisterAutoCloseDocument: ƒ}
6
+ // mode: "action"
7
+ // uiMode: "never"
8
+ // finalizeArguments: ƒ ()
9
+ // isCancelled: undefined
10
+ // onCancel: undefined
11
+ // reject: ƒ ()
12
+ // reportProgress: ƒ ()
13
+ // resolve: ƒ ()
14
+
15
+ export type CorrectExecutionContext = {
16
+ /**
17
+ * True if user has cancelled the modal interaction.
18
+ *
19
+ * User can cancel by hitting the Escape key, or by pressing the "Cancel" button in the progress bar.
20
+ */
21
+ isCancelled: boolean;
22
+ /**
23
+ * If assigned a method, it will be called when user cancels the modal interaction.
24
+ */
25
+ onCancel: () => void;
26
+ /**
27
+ * Call this to customize the progress bar.
28
+ */
29
+ reportProgress: (info: {
30
+ value: number;
31
+ commandName?: string;
32
+ }) => void;
33
+ /**
34
+ * Use the methods in here to control Photoshop state
35
+ */
36
+ hostControl: {
37
+ /**
38
+ * Call to suspend history on a target document, returns the suspension ID which can be used for resumeHistory
39
+ */
40
+ suspendHistory: (info: {
41
+ documentID: number,
42
+ name: string,
43
+ }) => Promise<number>;
44
+ /**
45
+ * Call to resume history on a target document
46
+ */
47
+ resumeHistory: (suspensionID: number, commit: boolean) => Promise<void>;
48
+ /** Register a document to be closed when the modal scope exits. See below for details. */
49
+ registerAutoCloseDocument: (documentID: number) => Promise<void>;
50
+ /** Unregister a document from being closed when the modal scope exits */
51
+ unregisterAutoCloseDocument: (documentID: number) => Promise<void>;
52
+ };
53
+ }
54
+
55
+ export type CorrectExecuteAsModalOptions = {
56
+ commandName: string;
57
+ interactive?: boolean;
58
+ timeOut?: number;
59
+ }
60
+
61
+ export type ExtendedExecutionContext = Omit<CorrectExecutionContext, "onCancel"> & ReturnType<typeof createModifyingBatchPlayContext> & {
62
+ signal: AbortSignal;
63
+ };
64
+
65
+ const originalExecuteAsModal: <T>(fn: (executionContext: CorrectExecutionContext) => Promise<void>, opts: CorrectExecuteAsModalOptions) => Promise<T> = core.executeAsModal as any;
66
+
67
+ type OptionsWithoutCommandName = Omit<CorrectExecuteAsModalOptions, "commandName">;
68
+
69
+ export async function executeAsModal<T>(commandName: string, fn: (executionContext: ExtendedExecutionContext) => Promise<T>, opts?: OptionsWithoutCommandName): Promise<T> {
70
+ let error: unknown;
71
+ let result: T;
72
+ await originalExecuteAsModal(async (executionContext) => {
73
+ const abortController = new AbortController();
74
+ executionContext.onCancel = () => {
75
+ abortController.abort();
76
+ };
77
+ // we cannot do a spread here, because not all properties are enumerable
78
+ const extendedExecutionContext: ExtendedExecutionContext = {
79
+ isCancelled: executionContext.isCancelled,
80
+ reportProgress: executionContext.reportProgress,
81
+ hostControl: executionContext.hostControl,
82
+ signal: abortController.signal,
83
+ ...createModifyingBatchPlayContext(),
84
+ };
85
+ try {
86
+ result = await fn(extendedExecutionContext);
87
+ } catch (e) {
88
+ console.error("error in executeAsModal");
89
+ console.error(e);
90
+ error = e;
91
+ }
92
+ }, {
93
+ commandName,
94
+ ...opts,
95
+ });
96
+ if (error) {
97
+ throw error;
98
+ } else {
99
+ return result!;
100
+ }
101
+ }
@@ -0,0 +1,15 @@
1
+ import type { Document } from "photoshop/dom/Document";
2
+
3
+ // The Adobe provided type is wrong
4
+ export type SuspendHistoryContext = {};
5
+ export async function suspendHistory<T>(
6
+ document: Document,
7
+ historyStateName: string,
8
+ fn: (context: SuspendHistoryContext) => Promise<T>
9
+ ): Promise<T> {
10
+ let result: T | undefined;
11
+ await document.suspendHistory(async (context) => {
12
+ result = await fn(context);
13
+ }, historyStateName);
14
+ return result!;
15
+ }
@@ -0,0 +1,18 @@
1
+ import type { Test } from "@bubblydoo/uxp-test-framework";
2
+ import { expect } from "chai";
3
+ import { suspendHistory } from "./suspendHistory";
4
+ import { app } from "photoshop";
5
+
6
+ export const suspendHistoryTest: Test = {
7
+ name: "core/suspendHistory should return correctly",
8
+ async run() {
9
+ const document = app.activeDocument;
10
+ if (!document) {
11
+ throw new Error("No active document");
12
+ }
13
+ const result = await suspendHistory(document, "Test", async (context) => {
14
+ return 'test';
15
+ });
16
+ expect(result).to.equal('test');
17
+ },
18
+ };
@@ -0,0 +1,11 @@
1
+ import type { Document } from "photoshop/dom/Document";
2
+ import { suspendHistory, type SuspendHistoryContext } from "../core/suspendHistory";
3
+ import { executeAsModal, type ExtendedExecutionContext } from "../core/executeAsModal";
4
+
5
+ type CombinedFn<T> = (executionContext: ExtendedExecutionContext, suspendHistoryContext: SuspendHistoryContext) => Promise<T>;
6
+
7
+ export const executeAsModalAndSuspendHistory = async <T>(commandName: string, document: Document, fn: CombinedFn<T>): Promise<T> => {
8
+ return await executeAsModal(commandName, async (ctx) => {
9
+ return await suspendHistory(document, commandName, (suspendHistoryContext) => fn(ctx, suspendHistoryContext));
10
+ });
11
+ };
@@ -0,0 +1,43 @@
1
+ import { constants } from "photoshop";
2
+ import type { Layer as DomLayer } from "photoshop/dom/Layer";
3
+
4
+ // get all layers (including nested in groups)
5
+ // TODO: I would rename this to getAllDOMLayers to avoid confusion with UXPToolkitLayer
6
+ export const getFlattenedDomLayersList = (
7
+ layers: DomLayer[],
8
+ ): DomLayer[] => {
9
+ const allLayers: DomLayer[] = [];
10
+
11
+ // Use a stack to avoid maximal call stack size (recursion) errors efficiently
12
+ const stack: DomLayer[] = [];
13
+
14
+ // Initialize stack with input layers in reverse order so the first layer is popped first.
15
+ // We use a manual loop because 'layers' can be a Photoshop 'Layers' collection object
16
+ // or an Array, and this covers both efficiently.
17
+ for (let i = layers.length - 1; i >= 0; i--) {
18
+ stack.push(layers[i]!);
19
+ }
20
+
21
+ while (stack.length > 0) {
22
+ const layer = stack.pop();
23
+ if (!layer) continue;
24
+
25
+ allLayers.push(layer);
26
+
27
+ // Check for group
28
+ if (layer.kind === constants.LayerKind.GROUP) {
29
+ // PERF: Accessing '.layers' on a DOM object is an expensive IPC call.
30
+ // We must cache it in a variable ensuring we only access it ONCE per group.
31
+ const children = layer.layers;
32
+
33
+ if (children && children.length > 0) {
34
+ // Push children in reverse to maintain depth-first standard order
35
+ for (let i = children.length - 1; i >= 0; i--) {
36
+ stack.push(children[i]!);
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ return allLayers;
43
+ };
@@ -0,0 +1,18 @@
1
+ import type { Layer as DomLayer } from "photoshop/dom/Layer";
2
+ import type { Tree } from "../general-tree/treeTypes";
3
+
4
+ // get layers recursively
5
+ export const photoshopDomLayersToTree = (layers: DomLayer[]): Tree<DomLayer> => {
6
+ // Get top-level layers
7
+ const filteredLayers = layers.filter((layer) => layer.parent === null);
8
+
9
+ const generateTree = (layers: DomLayer[]): Tree<DomLayer> => {
10
+ return layers.map((layer: DomLayer) => ({
11
+ ref: layer,
12
+ name: layer.name,
13
+ ...(layer.layers && { children: generateTree(layer.layers) }),
14
+ }));
15
+ };
16
+
17
+ return generateTree(filteredLayers);
18
+ };
@@ -0,0 +1,100 @@
1
+ import ErrorStackParser from "error-stack-parser";
2
+ import { SourceMapConsumer } from "source-map-js";
3
+ import { storage } from "uxp";
4
+ import { pathResolve } from "../node-compat/path/resolvePath";
5
+ import { UTError } from "../errors/ut-error";
6
+
7
+ export type BasicStackFrame = Pick<
8
+ ErrorStackParser.StackFrame,
9
+ "functionName" | "fileName" | "lineNumber" | "columnNumber"
10
+ >;
11
+
12
+ export async function parseUxpErrorSourcemaps(error: Error, opts: { unsourcemappedHeaderLines?: number } = {}) {
13
+ const parsedError = parseErrorIntoBasicStackFrames(error);
14
+
15
+ const unsourcemappedHeaderLines = opts.unsourcemappedHeaderLines ?? 0;
16
+
17
+ const loadedFilesCache: Record<string, storage.File> = {};
18
+
19
+ const fs = (storage as any).localFileSystem;
20
+ const parsedMappedError: BasicStackFrame[] = [];
21
+ for (const frame of parsedError) {
22
+ if (!frame.fileName || !frame.lineNumber || !frame.columnNumber) {
23
+ parsedMappedError.push(frame);
24
+ continue;
25
+ }
26
+ const entryPath = "plugin:" + frame.fileName;
27
+ const file =
28
+ loadedFilesCache[entryPath] ??
29
+ ((await fs.getEntryWithUrl(entryPath)) as storage.File);
30
+ loadedFilesCache[entryPath] = file;
31
+ if (!file.isFile) {
32
+ parsedMappedError.push(frame);
33
+ continue;
34
+ }
35
+ const sourcemapFileEntryPath = entryPath + ".map";
36
+ const sourcemapFile =
37
+ loadedFilesCache[sourcemapFileEntryPath] ??
38
+ ((await fs.getEntryWithUrl(sourcemapFileEntryPath)) as storage.File);
39
+ loadedFilesCache[sourcemapFileEntryPath] = sourcemapFile;
40
+ if (!sourcemapFile.isFile) {
41
+ parsedMappedError.push(frame);
42
+ continue;
43
+ }
44
+ const sourcemapContents = (await sourcemapFile.read({})) as string;
45
+ const sourcemap = JSON.parse(sourcemapContents);
46
+ const smc = new SourceMapConsumer(sourcemap);
47
+ const mappedFrame = smc.originalPositionFor({
48
+ line: frame.lineNumber - unsourcemappedHeaderLines,
49
+ column: frame.columnNumber,
50
+ });
51
+ if (mappedFrame.source && mappedFrame.line && mappedFrame.column) {
52
+ parsedMappedError.push({
53
+ ...frame,
54
+ fileName: mappedFrame.source,
55
+ lineNumber: mappedFrame.line,
56
+ columnNumber: mappedFrame.column,
57
+ } as ErrorStackParser.StackFrame);
58
+ } else {
59
+ parsedMappedError.push(frame);
60
+ }
61
+ }
62
+
63
+ return parsedMappedError;
64
+ }
65
+
66
+ function parseErrorIntoBasicStackFrames(error: Error): BasicStackFrame[] {
67
+ try {
68
+ const frames = ErrorStackParser.parse(error);
69
+ return frames.map((frame) => {
70
+ return {
71
+ functionName: frame.functionName,
72
+ fileName: frame.fileName,
73
+ lineNumber: frame.lineNumber,
74
+ columnNumber: frame.columnNumber,
75
+ };
76
+ });
77
+ } catch (e) {
78
+ throw new UTStacktraceParsingError("Failed to parse error stack trace", { cause: e });
79
+ }
80
+ }
81
+
82
+ export async function getBasicStackFrameAbsoluteFilePath(frame: BasicStackFrame): Promise<string> {
83
+ const pluginFolder = await (
84
+ storage as any
85
+ ).localFileSystem.getPluginFolder();
86
+ const absoluteFileName = pathResolve(
87
+ pluginFolder.nativePath,
88
+ "index.js",
89
+ frame.fileName!,
90
+ ).replace(/^plugin:/, "");
91
+ return `${absoluteFileName}:${frame.lineNumber}:${frame.columnNumber}`;
92
+ }
93
+
94
+ export class UTStacktraceParsingError extends UTError {
95
+ public override readonly name = "UTStacktraceParsingError";
96
+
97
+ constructor(message: string, opts: ErrorOptions = {}) {
98
+ super(message, opts);
99
+ }
100
+ }
@@ -0,0 +1,24 @@
1
+ import type { Test } from "@bubblydoo/uxp-test-framework";
2
+ import { parseUxpErrorSourcemaps } from "./sourcemaps";
3
+ import { expect } from "chai";
4
+
5
+ function throwError() {
6
+ throw new Error("Uncaught error"); // this error should stay exactly here in the source code, see below
7
+ }
8
+
9
+ export const sourcemapsTest: Test = {
10
+ name: "sourcemaps: should parse error sourcemaps",
11
+ async run() {
12
+ let error: Error;
13
+ try {
14
+ throwError();
15
+ } catch (e) {
16
+ error = e as Error;
17
+ }
18
+ const parsedError = await parseUxpErrorSourcemaps(error!);
19
+ console.log(parsedError);
20
+ expect(parsedError[0]!.fileName).to.include("sourcemaps.uxp-test.ts");
21
+ expect(parsedError[0]!.lineNumber).to.eq(6);
22
+ expect(parsedError[0]!.columnNumber).to.eq(8);
23
+ },
24
+ };
@@ -0,0 +1,6 @@
1
+ export class UTError extends Error {
2
+ constructor(message: string, opts: ErrorOptions = {}) {
3
+ super(message, opts);
4
+ this.name = "UTError";
5
+ }
6
+ }
@@ -0,0 +1,10 @@
1
+ import { app } from "photoshop";
2
+ import { storage } from "uxp";
3
+ import { executeAsModal } from "../core/executeAsModal";
4
+
5
+ export async function openFileByPath(path: string) {
6
+ const fs = (storage as any).localFileSystem;
7
+ const file = (await fs.getEntryWithUrl(path)) as storage.File;
8
+ const doc = await executeAsModal("Open file", () => app.open(file as any));
9
+ return doc;
10
+ }
@@ -0,0 +1,12 @@
1
+ import type { Tree } from "./treeTypes";
2
+
3
+ export function flattenTree<T>(tree: Tree<T>): Tree<T>[0][] {
4
+ const result: Tree<T>[0][] = [];
5
+ for (const node of tree) {
6
+ result.push(node);
7
+ if (node.children) {
8
+ result.push(...flattenTree(node.children));
9
+ }
10
+ }
11
+ return result;
12
+ }
@@ -0,0 +1,4 @@
1
+ export type LayerRef = {
2
+ id: number;
3
+ docId: number;
4
+ };
@@ -0,0 +1,11 @@
1
+ import type { Tree } from "./treeTypes";
2
+
3
+ export function mapTree<TRef, TMappedRef>(tree: Tree<TRef>, mapFn: (node: TRef) => TMappedRef): Tree<TMappedRef> {
4
+ return tree.map((node) => {
5
+ return {
6
+ ...node,
7
+ ref: mapFn(node.ref),
8
+ children: node.children ? mapTree(node.children, mapFn) : undefined,
9
+ };
10
+ });
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { Tree } from "./treeTypes";
2
+
3
+ export function mapTreeRef<TRef, TMappedRef>(tree: Tree<TRef>, mapFn: (node: TRef) => TMappedRef): Tree<TMappedRef> {
4
+ return tree.map((node) => {
5
+ return {
6
+ ...node,
7
+ ref: mapFn(node.ref),
8
+ children: node.children ? mapTreeRef(node.children, mapFn) : undefined,
9
+ };
10
+ });
11
+ }