@khanacademy/perseus-editor 34.2.1 → 35.0.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/dist/article-editor.d.ts +39 -3
- package/dist/editor-page.d.ts +3 -2
- package/dist/es/index.js +26 -24
- package/dist/es/index.js.map +1 -1
- package/dist/hint-editor.d.ts +1 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -24
- package/dist/index.js.map +1 -1
- package/dist/item-editor.d.ts +11 -4
- package/dist/preview/message-types.d.ts +43 -15
- package/dist/preview/preview-data-sanitizer.d.ts +0 -5
- package/dist/preview/sanitize-api-options.d.ts +14 -1
- package/dist/preview/use-preview-presenter.d.ts +6 -6
- package/dist/preview-with-iframe.d.ts +3 -2
- package/dist/testing/preview/exercise-preview-page.d.ts +2 -4
- package/dist/testing/preview/preview-renderer.d.ts +4 -2
- package/package.json +38 -38
package/dist/item-editor.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import Editor from "./editor";
|
|
3
|
-
import IframeContentRenderer from "./iframe-content-renderer";
|
|
4
3
|
import ItemExtrasEditor from "./item-extras-editor";
|
|
4
|
+
import type { PreviewContent } from "./preview/message-types";
|
|
5
5
|
import type { APIOptions, ImageUploader, ChangeHandler, DeviceType } from "@khanacademy/perseus";
|
|
6
6
|
import type { PerseusAnswerArea, PerseusRenderer, PerseusItem } from "@khanacademy/perseus-core";
|
|
7
|
+
type QuestionPreviewData = Extract<PreviewContent, {
|
|
8
|
+
type: "question";
|
|
9
|
+
}>;
|
|
7
10
|
type Props = {
|
|
8
11
|
/** Additional templates that the host application would like to display
|
|
9
12
|
* within the Perseus Editor.
|
|
10
13
|
*/
|
|
11
14
|
additionalTemplates?: Record<string, string>;
|
|
12
15
|
apiOptions?: APIOptions;
|
|
13
|
-
deviceType
|
|
16
|
+
deviceType: DeviceType;
|
|
14
17
|
widgetIsOpen?: boolean;
|
|
15
18
|
imageUploader?: ImageUploader;
|
|
16
19
|
question?: PerseusRenderer;
|
|
@@ -22,6 +25,11 @@ type Props = {
|
|
|
22
25
|
* for non-content library exercise questions.
|
|
23
26
|
*/
|
|
24
27
|
itemId?: string;
|
|
28
|
+
/** Whether to highlight lint warnings in the preview. */
|
|
29
|
+
highlightLint: boolean;
|
|
30
|
+
/** The problem number, used for deterministic random seeding in the
|
|
31
|
+
* preview. */
|
|
32
|
+
problemNum?: number;
|
|
25
33
|
};
|
|
26
34
|
declare class ItemEditor extends React.Component<Props> {
|
|
27
35
|
static defaultProps: {
|
|
@@ -29,11 +37,9 @@ declare class ItemEditor extends React.Component<Props> {
|
|
|
29
37
|
onChange: () => void;
|
|
30
38
|
question: Record<any, any>;
|
|
31
39
|
};
|
|
32
|
-
frame: React.RefObject<IframeContentRenderer>;
|
|
33
40
|
questionEditor: React.RefObject<Editor>;
|
|
34
41
|
itemExtrasEditor: React.RefObject<ItemExtrasEditor>;
|
|
35
42
|
updateProps: (newProps: Partial<PerseusItem>) => void;
|
|
36
|
-
triggerPreviewUpdate: (newData?: any) => void;
|
|
37
43
|
handleEditorChange: ChangeHandler;
|
|
38
44
|
handleItemExtrasChange: (newProps: Partial<PerseusAnswerArea>) => void;
|
|
39
45
|
getSaveWarnings: () => any;
|
|
@@ -41,6 +47,7 @@ declare class ItemEditor extends React.Component<Props> {
|
|
|
41
47
|
answerArea: PerseusAnswerArea;
|
|
42
48
|
question: PerseusRenderer;
|
|
43
49
|
};
|
|
50
|
+
_derivePreviewContent(): QuestionPreviewData | null;
|
|
44
51
|
render(): React.ReactNode;
|
|
45
52
|
}
|
|
46
53
|
export default ItemEditor;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* This file contains the various message types passed between the editor and
|
|
3
3
|
* the preview iframe.
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
6
|
-
import type { Hint,
|
|
5
|
+
import type { SerializableApiOptions } from "./sanitize-api-options";
|
|
6
|
+
import type { Hint, PerseusItem, PerseusRenderer } from "@khanacademy/perseus-core";
|
|
7
7
|
/**
|
|
8
8
|
* The subset of `LinterContextProps` that's meaningful to send across the
|
|
9
9
|
* preview bridge. The receiver wraps this in `pushContextStack(...)` before
|
|
@@ -24,13 +24,16 @@ interface PreviewMessageBase {
|
|
|
24
24
|
source: typeof PREVIEW_MESSAGE_SOURCE;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Data for question preview
|
|
27
|
+
* Data for the read-only question preview shown while editing an exercise.
|
|
28
|
+
*
|
|
29
|
+
* This carries just the question's `PerseusRenderer` — the content under edit —
|
|
30
|
+
* rather than a full `PerseusItem`. The answer widgets live inside the question
|
|
31
|
+
* renderer itself; the legacy answer-area tools and hints are previewed
|
|
32
|
+
* elsewhere.
|
|
28
33
|
*/
|
|
29
34
|
export type QuestionPreviewData = {
|
|
30
|
-
|
|
31
|
-
apiOptions:
|
|
32
|
-
initialHintsVisible: number;
|
|
33
|
-
device: DeviceType;
|
|
35
|
+
question: PerseusRenderer;
|
|
36
|
+
apiOptions: SerializableApiOptions;
|
|
34
37
|
linterContext: PreviewLinterContext;
|
|
35
38
|
reviewMode?: boolean;
|
|
36
39
|
legacyPerseusLint?: ReadonlyArray<string>;
|
|
@@ -42,18 +45,40 @@ export type QuestionPreviewData = {
|
|
|
42
45
|
export type HintPreviewData = {
|
|
43
46
|
hint: Hint;
|
|
44
47
|
pos: number;
|
|
45
|
-
apiOptions:
|
|
48
|
+
apiOptions: SerializableApiOptions;
|
|
46
49
|
linterContext: PreviewLinterContext;
|
|
47
50
|
};
|
|
48
51
|
/**
|
|
49
|
-
* Data for article section preview
|
|
52
|
+
* Data for a single article section preview (used in edit mode for one section)
|
|
50
53
|
*/
|
|
51
|
-
export type
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
export type ArticleSectionPreviewData = {
|
|
55
|
+
article: PerseusRenderer;
|
|
56
|
+
apiOptions: SerializableApiOptions;
|
|
54
57
|
linterContext: PreviewLinterContext;
|
|
55
58
|
legacyPerseusLint?: ReadonlyArray<string>;
|
|
56
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Data for article-all preview (used in preview mode to render all sections at once)
|
|
62
|
+
*/
|
|
63
|
+
export type ArticleAllPreviewData = {
|
|
64
|
+
article: ReadonlyArray<PerseusRenderer>;
|
|
65
|
+
apiOptions: SerializableApiOptions;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Data for the interactive exercise preview (drives the exercise editor's
|
|
69
|
+
* "Preview" tab in khan/frontend).
|
|
70
|
+
*
|
|
71
|
+
* Unlike the read-only `question` preview, this signals the iframe to mount the
|
|
72
|
+
* full interactive `ExercisePreview` component (hint controls, answer checking,
|
|
73
|
+
* scoring) rather than a `ServerItemRenderer`. All of that interactivity lives
|
|
74
|
+
* inside the iframe — the parent only sends these fields and never observes or
|
|
75
|
+
* controls the exercise state.
|
|
76
|
+
*/
|
|
77
|
+
export type ExercisePreviewData = {
|
|
78
|
+
item: PerseusItem;
|
|
79
|
+
apiOptions: SerializableApiOptions;
|
|
80
|
+
showRationales: boolean;
|
|
81
|
+
};
|
|
57
82
|
/**
|
|
58
83
|
* Union of all preview content types
|
|
59
84
|
*/
|
|
@@ -64,11 +89,14 @@ export type PreviewContent = {
|
|
|
64
89
|
type: "hint";
|
|
65
90
|
data: HintPreviewData;
|
|
66
91
|
} | {
|
|
67
|
-
type: "article";
|
|
68
|
-
data:
|
|
92
|
+
type: "article-section";
|
|
93
|
+
data: ArticleSectionPreviewData;
|
|
69
94
|
} | {
|
|
70
95
|
type: "article-all";
|
|
71
|
-
data:
|
|
96
|
+
data: ArticleAllPreviewData;
|
|
97
|
+
} | {
|
|
98
|
+
type: "exercise";
|
|
99
|
+
data: ExercisePreviewData;
|
|
72
100
|
};
|
|
73
101
|
/**
|
|
74
102
|
* Message from parent sending content data to iframe
|
|
@@ -2,11 +2,6 @@ import type { PreviewContent } from "./message-types";
|
|
|
2
2
|
/**
|
|
3
3
|
* Sanitizes preview content by removing non-serializable functions and React
|
|
4
4
|
* components from apiOptions before sending via postMessage.
|
|
5
|
-
*
|
|
6
|
-
* NOTE: The article-all case currently sanitizes apiOptions on each section
|
|
7
|
-
* individually because the current type is ArticlePreviewData[] (each section
|
|
8
|
-
* carries its own apiOptions). This will be simplified to a single apiOptions
|
|
9
|
-
* when we restructure the preview data types.
|
|
10
5
|
*/
|
|
11
6
|
export declare function sanitizePreviewData(
|
|
12
7
|
/** Preview content to sanitize */
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { APIOptions } from "@khanacademy/perseus";
|
|
2
|
+
/**
|
|
3
|
+
* The `APIOptions` fields that can't survive `postMessage`'s structured-clone
|
|
4
|
+
* algorithm — function callbacks and React nodes. `sanitizeApiOptions` strips
|
|
5
|
+
* exactly these, so keep this list and the destructuring below in sync.
|
|
6
|
+
*/
|
|
7
|
+
type NonSerializableApiOptionKey = "onFocusChange" | "answerableCallback" | "getAnotherHint" | "interactionCallback" | "trackInteraction" | "baseElements" | "imagePlaceholder" | "widgetPlaceholder";
|
|
8
|
+
/**
|
|
9
|
+
* `APIOptions` with the non-serializable fields removed. This is the shape that
|
|
10
|
+
* actually crosses the preview postMessage bridge, so the preview message types
|
|
11
|
+
* describe their `apiOptions` with this rather than the full `APIOptions`.
|
|
12
|
+
*/
|
|
13
|
+
export type SerializableApiOptions = Omit<APIOptions, NonSerializableApiOptionKey>;
|
|
2
14
|
/**
|
|
3
15
|
* Removes non-serializable functions and React components from APIOptions
|
|
4
16
|
* before sending via postMessage.
|
|
@@ -8,4 +20,5 @@ import type { APIOptions } from "@khanacademy/perseus";
|
|
|
8
20
|
*
|
|
9
21
|
* Serializable options (booleans, strings, numbers) are preserved.
|
|
10
22
|
*/
|
|
11
|
-
export declare function sanitizeApiOptions(apiOptions: APIOptions):
|
|
23
|
+
export declare function sanitizeApiOptions(apiOptions: APIOptions): SerializableApiOptions;
|
|
24
|
+
export {};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { PreviewContent } from "./message-types";
|
|
2
2
|
type UsePreviewPresenterResult = {
|
|
3
3
|
/**
|
|
4
|
-
* The preview content
|
|
4
|
+
* The preview content received from the parent, or null if not yet loaded
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
content: PreviewContent | null;
|
|
7
7
|
/**
|
|
8
8
|
* Whether the preview should render in mobile mode (from data-mobile attribute)
|
|
9
9
|
*/
|
|
@@ -29,18 +29,18 @@ type UsePreviewPresenterResult = {
|
|
|
29
29
|
* @example
|
|
30
30
|
* ```tsx
|
|
31
31
|
* function PreviewPage() {
|
|
32
|
-
* const {
|
|
32
|
+
* const { content, isMobile, reportHeight } = usePreviewPresenter();
|
|
33
33
|
*
|
|
34
34
|
* React.useEffect(() => {
|
|
35
35
|
* if (containerRef.current) {
|
|
36
36
|
* reportHeight(containerRef.current.scrollHeight);
|
|
37
37
|
* }
|
|
38
|
-
* }, [
|
|
38
|
+
* }, [content, reportHeight]);
|
|
39
39
|
*
|
|
40
|
-
* if (!
|
|
40
|
+
* if (!content) return <div>Loading...</div>;
|
|
41
41
|
* return <PreviewRenderer
|
|
42
42
|
* ref={containerRef}
|
|
43
|
-
*
|
|
43
|
+
* content={content}
|
|
44
44
|
* isMobile={isMobile}
|
|
45
45
|
* />;
|
|
46
46
|
* }
|
|
@@ -15,9 +15,10 @@ type Props = {
|
|
|
15
15
|
*/
|
|
16
16
|
seamless: boolean;
|
|
17
17
|
/**
|
|
18
|
-
* The content to render in the preview area.
|
|
18
|
+
* The content to render in the preview area. `null` means no data has
|
|
19
|
+
* been provided yet and the iframe should render an empty placeholder.
|
|
19
20
|
*/
|
|
20
|
-
content: PreviewContent;
|
|
21
|
+
content: PreviewContent | null;
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* Displays content in an iframe, isolating it from the parent page and
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This is loaded inside the iframe, where it sets up the ExercisePreviewFrame
|
|
6
|
-
* component that handles all communication between the iframe and its parent.
|
|
3
|
+
* The exercise preview page. This page is loaded inside an iframe and should
|
|
4
|
+
* only be used to support editor previews in Storybook!
|
|
7
5
|
*/
|
|
8
6
|
declare const ExercisePreviewPage: () => React.JSX.Element;
|
|
9
7
|
export default ExercisePreviewPage;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { PreviewContent } from "../../preview/message-types";
|
|
3
3
|
type Props = {
|
|
4
|
-
|
|
4
|
+
content: PreviewContent;
|
|
5
|
+
isMobile: boolean;
|
|
6
|
+
hasLintGutter: boolean;
|
|
5
7
|
};
|
|
6
8
|
/**
|
|
7
9
|
* Renders the appropriate content based on preview data type
|
|
8
10
|
*/
|
|
9
|
-
export declare function PreviewRenderer({
|
|
11
|
+
export declare function PreviewRenderer({ content, isMobile, hasLintGutter }: Props): React.JSX.Element | null;
|
|
10
12
|
export {};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Perseus editors",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "35.0.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -38,33 +38,33 @@
|
|
|
38
38
|
"@khanacademy/kas": "2.2.3",
|
|
39
39
|
"@khanacademy/keypad-context": "3.2.73",
|
|
40
40
|
"@khanacademy/kmath": "2.4.32",
|
|
41
|
-
"@khanacademy/math-input": "26.6.
|
|
42
|
-
"@khanacademy/perseus": "80.0.
|
|
41
|
+
"@khanacademy/math-input": "26.6.1",
|
|
42
|
+
"@khanacademy/perseus": "80.0.1",
|
|
43
43
|
"@khanacademy/perseus-core": "31.0.0",
|
|
44
44
|
"@khanacademy/perseus-linter": "5.1.13",
|
|
45
45
|
"@khanacademy/perseus-utils": "2.1.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@khanacademy/mathjax-renderer": "3.0.0",
|
|
49
|
-
"@khanacademy/wonder-blocks-accordion": "3.1.
|
|
50
|
-
"@khanacademy/wonder-blocks-badge": "1.1.
|
|
51
|
-
"@khanacademy/wonder-blocks-banner": "5.1.
|
|
52
|
-
"@khanacademy/wonder-blocks-button": "11.7.
|
|
53
|
-
"@khanacademy/wonder-blocks-clickable": "8.2.
|
|
49
|
+
"@khanacademy/wonder-blocks-accordion": "3.1.67",
|
|
50
|
+
"@khanacademy/wonder-blocks-badge": "1.1.19",
|
|
51
|
+
"@khanacademy/wonder-blocks-banner": "5.1.9",
|
|
52
|
+
"@khanacademy/wonder-blocks-button": "11.7.5",
|
|
53
|
+
"@khanacademy/wonder-blocks-clickable": "8.2.7",
|
|
54
54
|
"@khanacademy/wonder-blocks-core": "12.4.4",
|
|
55
|
-
"@khanacademy/wonder-blocks-dropdown": "10.11.
|
|
56
|
-
"@khanacademy/wonder-blocks-form": "7.6.
|
|
57
|
-
"@khanacademy/wonder-blocks-icon": "5.3.
|
|
58
|
-
"@khanacademy/wonder-blocks-icon-button": "11.4.
|
|
59
|
-
"@khanacademy/wonder-blocks-labeled-field": "4.1.
|
|
60
|
-
"@khanacademy/wonder-blocks-layout": "3.1.
|
|
61
|
-
"@khanacademy/wonder-blocks-link": "10.3.
|
|
62
|
-
"@khanacademy/wonder-blocks-pill": "3.1.
|
|
63
|
-
"@khanacademy/wonder-blocks-switch": "3.4.
|
|
55
|
+
"@khanacademy/wonder-blocks-dropdown": "10.11.6",
|
|
56
|
+
"@khanacademy/wonder-blocks-form": "7.6.8",
|
|
57
|
+
"@khanacademy/wonder-blocks-icon": "5.3.21",
|
|
58
|
+
"@khanacademy/wonder-blocks-icon-button": "11.4.3",
|
|
59
|
+
"@khanacademy/wonder-blocks-labeled-field": "4.1.7",
|
|
60
|
+
"@khanacademy/wonder-blocks-layout": "3.1.58",
|
|
61
|
+
"@khanacademy/wonder-blocks-link": "10.3.7",
|
|
62
|
+
"@khanacademy/wonder-blocks-pill": "3.1.71",
|
|
63
|
+
"@khanacademy/wonder-blocks-switch": "3.4.7",
|
|
64
64
|
"@khanacademy/wonder-blocks-timing": "7.1.0",
|
|
65
|
-
"@khanacademy/wonder-blocks-tokens": "17.
|
|
66
|
-
"@khanacademy/wonder-blocks-tooltip": "4.1.
|
|
67
|
-
"@khanacademy/wonder-blocks-typography": "5.0.
|
|
65
|
+
"@khanacademy/wonder-blocks-tokens": "17.1.0",
|
|
66
|
+
"@khanacademy/wonder-blocks-tooltip": "4.1.84",
|
|
67
|
+
"@khanacademy/wonder-blocks-typography": "5.0.1",
|
|
68
68
|
"@khanacademy/wonder-stuff-core": "3.0.0",
|
|
69
69
|
"@phosphor-icons/core": "2.0.2",
|
|
70
70
|
"aphrodite": "1.2.5",
|
|
@@ -79,25 +79,25 @@
|
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"@khanacademy/mathjax-renderer": "^3.0.0",
|
|
82
|
-
"@khanacademy/wonder-blocks-accordion": "^3.1.
|
|
83
|
-
"@khanacademy/wonder-blocks-badge": "^1.1.
|
|
84
|
-
"@khanacademy/wonder-blocks-banner": "^5.1.
|
|
85
|
-
"@khanacademy/wonder-blocks-button": "^11.7.
|
|
86
|
-
"@khanacademy/wonder-blocks-clickable": "^8.2.
|
|
82
|
+
"@khanacademy/wonder-blocks-accordion": "^3.1.67",
|
|
83
|
+
"@khanacademy/wonder-blocks-badge": "^1.1.19",
|
|
84
|
+
"@khanacademy/wonder-blocks-banner": "^5.1.9",
|
|
85
|
+
"@khanacademy/wonder-blocks-button": "^11.7.5",
|
|
86
|
+
"@khanacademy/wonder-blocks-clickable": "^8.2.7",
|
|
87
87
|
"@khanacademy/wonder-blocks-core": "^12.4.4",
|
|
88
|
-
"@khanacademy/wonder-blocks-dropdown": "^10.11.
|
|
89
|
-
"@khanacademy/wonder-blocks-form": "^7.6.
|
|
90
|
-
"@khanacademy/wonder-blocks-icon": "^5.3.
|
|
91
|
-
"@khanacademy/wonder-blocks-icon-button": "^11.4.
|
|
92
|
-
"@khanacademy/wonder-blocks-labeled-field": "^4.1.
|
|
93
|
-
"@khanacademy/wonder-blocks-layout": "^3.1.
|
|
94
|
-
"@khanacademy/wonder-blocks-link": "^10.3.
|
|
95
|
-
"@khanacademy/wonder-blocks-pill": "^3.1.
|
|
96
|
-
"@khanacademy/wonder-blocks-switch": "^3.4.
|
|
88
|
+
"@khanacademy/wonder-blocks-dropdown": "^10.11.6",
|
|
89
|
+
"@khanacademy/wonder-blocks-form": "^7.6.8",
|
|
90
|
+
"@khanacademy/wonder-blocks-icon": "^5.3.21",
|
|
91
|
+
"@khanacademy/wonder-blocks-icon-button": "^11.4.3",
|
|
92
|
+
"@khanacademy/wonder-blocks-labeled-field": "^4.1.7",
|
|
93
|
+
"@khanacademy/wonder-blocks-layout": "^3.1.58",
|
|
94
|
+
"@khanacademy/wonder-blocks-link": "^10.3.7",
|
|
95
|
+
"@khanacademy/wonder-blocks-pill": "^3.1.71",
|
|
96
|
+
"@khanacademy/wonder-blocks-switch": "^3.4.7",
|
|
97
97
|
"@khanacademy/wonder-blocks-timing": "^7.1.0",
|
|
98
|
-
"@khanacademy/wonder-blocks-tokens": "^17.
|
|
99
|
-
"@khanacademy/wonder-blocks-tooltip": "^4.1.
|
|
100
|
-
"@khanacademy/wonder-blocks-typography": "^5.0.
|
|
98
|
+
"@khanacademy/wonder-blocks-tokens": "^17.1.0",
|
|
99
|
+
"@khanacademy/wonder-blocks-tooltip": "^4.1.84",
|
|
100
|
+
"@khanacademy/wonder-blocks-typography": "^5.0.1",
|
|
101
101
|
"@khanacademy/wonder-stuff-core": "^3.0.0",
|
|
102
102
|
"@phosphor-icons/core": "^2.0.2",
|
|
103
103
|
"aphrodite": "^1.2.5",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
},
|
|
111
111
|
"keywords": [],
|
|
112
112
|
"khan": {
|
|
113
|
-
"catalogHash": "
|
|
113
|
+
"catalogHash": "c327b001d4b948a3"
|
|
114
114
|
},
|
|
115
115
|
"scripts": {}
|
|
116
116
|
}
|