@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.
- package/.turbo/turbo-build.log +15 -0
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +733 -0
- package/package.json +41 -0
- package/src/commands-library/getLayerProperties.ts +32 -0
- package/src/commands-library/renameLayer.ts +15 -0
- package/src/commands-library/renameLayer.uxp-test.ts +32 -0
- package/src/core/batchPlay.ts +16 -0
- package/src/core/command.ts +130 -0
- package/src/core/executeAsModal.ts +101 -0
- package/src/core/suspendHistory.ts +15 -0
- package/src/core/suspendHistory.uxp-test.ts +18 -0
- package/src/core-wrappers/executeAsModalAndSuspendHistory.ts +11 -0
- package/src/dom/getFlattenedDomLayersList.ts +43 -0
- package/src/dom/photoshopDomLayersToTree.ts +18 -0
- package/src/error-sourcemaps/sourcemaps.ts +100 -0
- package/src/error-sourcemaps/sourcemaps.uxp-test.ts +24 -0
- package/src/errors/ut-error.ts +6 -0
- package/src/filesystem/openFileByPath.ts +10 -0
- package/src/general-tree/flattenTree.ts +12 -0
- package/src/general-tree/layerRef.ts +4 -0
- package/src/general-tree/mapTree.ts +11 -0
- package/src/general-tree/mapTreeRef.ts +11 -0
- package/src/general-tree/treeTypes.ts +7 -0
- package/src/index.ts +73 -0
- package/src/metadata-storage/metadataStorage.ts +66 -0
- package/src/metadata-storage/metadataStorage.uxp-test.ts +35 -0
- package/src/node-compat/path/resolvePath.ts +19 -0
- package/src/other/applicationInfo.ts +169 -0
- package/src/other/applicationInfo.uxp-test.ts +11 -0
- package/src/other/clipboard.ts +10 -0
- package/src/other/clipboard.uxp-test.ts +17 -0
- package/src/other/uxpEntrypoints.ts +9 -0
- package/src/ut-tree/getFlattenedLayerDescriptorsList.ts +72 -0
- package/src/ut-tree/getLayerEffects.ts +41 -0
- package/src/ut-tree/getLayerProperties.ts +35 -0
- package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.ts +182 -0
- package/src/ut-tree/photoshopLayerDescriptorsToUTLayers.uxp-test.ts +52 -0
- package/src/ut-tree/psLayerRef.ts +4 -0
- package/src/ut-tree/utLayersToTree.ts +21 -0
- package/src/util/utLayerToLayer.ts +41 -0
- package/test/fixtures/clipping-layers.psd +0 -0
- package/test/fixtures/one-layer.psd +0 -0
- package/test/index.ts +21 -0
- package/test/meta-tests/executeAsModal.uxp-test.ts +38 -0
- package/test/meta-tests/suspendHistory.uxp-test.ts +27 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +9 -0
- 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,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,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
|
+
}
|