Implement interactive execution plan and strict HTML validation
Features: - **Interactive Checklist**: 'Review Changes' modal now updates in real-time as save tasks complete. - **Signal Logging**: Backend emits [SIGNAL] logs for deletions, adoptions, uploads, and reorders. - **UI Cleanup**: Removed redundant textual 'Execute Progress' log pane. Build & Quality: - **HTML Validation**: Added ools/validate_html.ts to build pipeline to prevent syntax errors in embedded JS. - **Strict Build**: pm run build now runs alidate:html first.
This commit is contained in:
527
package-lock.json
generated
527
package-lock.json
generated
@ -8,14 +8,17 @@
|
|||||||
"name": "product_inventory",
|
"name": "product_inventory",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cheerio": "^0.22.35",
|
||||||
"@types/google-apps-script": "^1.0.85",
|
"@types/google-apps-script": "^1.0.85",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"copy-webpack-plugin": "^13.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-html": "^8.1.3",
|
"eslint-plugin-html": "^8.1.3",
|
||||||
"gas-webpack-plugin": "^2.6.0",
|
"gas-webpack-plugin": "^2.6.0",
|
||||||
|
"glob": "^13.0.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
@ -771,6 +774,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/balanced-match": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/balanced-match": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@istanbuljs/load-nyc-config": {
|
"node_modules/@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
@ -1453,6 +1479,39 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jest/reporters/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jest/reporters/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jest/reporters/node_modules/jest-message-util": {
|
"node_modules/@jest/reporters/node_modules/jest-message-util": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
|
||||||
@ -1508,6 +1567,19 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jest/reporters/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jest/reporters/node_modules/pretty-format": {
|
"node_modules/@jest/reporters/node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@ -1861,6 +1933,16 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cheerio": {
|
||||||
|
"version": "0.22.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz",
|
||||||
|
"integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
@ -2721,6 +2803,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
@ -2875,6 +2964,50 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cheerio": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio-select": "^2.1.0",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"encoding-sniffer": "^0.2.1",
|
||||||
|
"htmlparser2": "^10.0.0",
|
||||||
|
"parse5": "^7.3.0",
|
||||||
|
"parse5-htmlparser2-tree-adapter": "^7.1.0",
|
||||||
|
"parse5-parser-stream": "^7.1.2",
|
||||||
|
"undici": "^7.12.0",
|
||||||
|
"whatwg-mimetype": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cheerio-select": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chrome-trace-event": {
|
"node_modules/chrome-trace-event": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||||
@ -3169,6 +3302,36 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -3374,6 +3537,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding-sniffer": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.2",
|
"version": "5.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||||
@ -4200,22 +4377,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "13.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs.realpath": "^1.0.0",
|
"minimatch": "^10.1.1",
|
||||||
"inflight": "^1.0.4",
|
"minipass": "^7.1.2",
|
||||||
"inherits": "2",
|
"path-scurry": "^2.0.0"
|
||||||
"minimatch": "^3.1.1",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@ -4241,28 +4414,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
@ -4454,6 +4619,19 @@
|
|||||||
"url": "https://github.com/sponsors/typicode"
|
"url": "https://github.com/sponsors/typicode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@ -5084,6 +5262,39 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-config/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jest-config/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-config/node_modules/jest-util": {
|
"node_modules/jest-config/node_modules/jest-util": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
|
||||||
@ -5102,6 +5313,19 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-config/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-config/node_modules/pretty-format": {
|
"node_modules/jest-config/node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@ -5805,6 +6029,39 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-runtime/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jest-runtime/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-runtime/node_modules/jest-message-util": {
|
"node_modules/jest-runtime/node_modules/jest-message-util": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
|
||||||
@ -5859,6 +6116,19 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-runtime/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-runtime/node_modules/pretty-format": {
|
"node_modules/jest-runtime/node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@ -6827,6 +7097,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -6898,6 +7178,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@ -7013,6 +7306,46 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-parser-stream": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -7050,6 +7383,33 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
|
"version": "11.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
@ -7421,6 +7781,52 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rimraf/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rimraf/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@ -7466,6 +7872,13 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
@ -7866,6 +8279,28 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude/node_modules/minimatch": {
|
"node_modules/test-exclude/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@ -8193,6 +8628,16 @@
|
|||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
@ -8417,6 +8862,30 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"global.ts"
|
"global.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run lint && webpack --mode production",
|
"validate:html": "ts-node tools/validate_html.ts",
|
||||||
|
"build": "npm run validate:html && npm run lint && webpack --mode production",
|
||||||
"lint": "eslint \"src/**/*.{ts,js,html}\"",
|
"lint": "eslint \"src/**/*.{ts,js,html}\"",
|
||||||
"deploy": "clasp push",
|
"deploy": "clasp push",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@ -14,14 +15,17 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cheerio": "^0.22.35",
|
||||||
"@types/google-apps-script": "^1.0.85",
|
"@types/google-apps-script": "^1.0.85",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"copy-webpack-plugin": "^13.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-html": "^8.1.3",
|
"eslint-plugin-html": "^8.1.3",
|
||||||
"gas-webpack-plugin": "^2.6.0",
|
"gas-webpack-plugin": "^2.6.0",
|
||||||
|
"glob": "^13.0.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
|||||||
@ -811,9 +811,14 @@
|
|||||||
alert("Script Error: " + msg + "\nLine: " + line);
|
alert("Script Error: " + msg + "\nLine: " + line);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Template Variables (Injected by Server)
|
||||||
|
var initialSku = "<?= initialSku ?>";
|
||||||
|
var initialTitle = "<?= initialTitle ?>";
|
||||||
|
|
||||||
// --- ES5 Refactor: MediaState ---
|
// --- ES5 Refactor: MediaState ---
|
||||||
function MediaState() {
|
function MediaState() {
|
||||||
this.sku = null;
|
this.sku = (initialSku && initialSku !== "undefined") ? initialSku : null;
|
||||||
|
this.title = (initialTitle && initialTitle !== "undefined") ? initialTitle : "";
|
||||||
this.token = null;
|
this.token = null;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.initialState = [];
|
this.initialState = [];
|
||||||
@ -1626,12 +1631,16 @@
|
|||||||
return '<div style="text-align:center; padding:20px;">No pending changes.</div>';
|
return '<div style="text-align:center; padding:20px;">No pending changes.</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderSection = (title, items, icon) => {
|
const renderSection = (title, items, icon, phaseKey) => {
|
||||||
if (!items || items.length === 0) return '';
|
if (!items || items.length === 0) return '';
|
||||||
return `
|
return `
|
||||||
<h4 style="margin:10px 0 5px; border-bottom:1px solid #eee;">${icon} ${title} (${items.length})</h4>
|
<h4 style="margin:10px 0 5px; border-bottom:1px solid #eee;">${icon} ${title} (${items.length})</h4>
|
||||||
<ul style="font-size:12px; padding-left:20px; color:#555;">
|
<ul style="font-size:12px; padding-left:20px; color:#555;">
|
||||||
${items.map(i => `<li>${i.filename}</li>`).join('')}
|
${items.map(function (i) {
|
||||||
|
var idSuffix = i.id || (i.filename ? i.filename.replace(/[^a-z0-9]/gi, '_') : 'unknown');
|
||||||
|
var liId = 'li-plan-' + (phaseKey || 'legacy') + '-' + idSuffix;
|
||||||
|
return '<li id="' + liId + '">' + (i.filename || 'Item') + ' <span class="plan-status"></span></li>';
|
||||||
|
}).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
@ -1654,10 +1663,10 @@
|
|||||||
if (plan.deletions || plan.adoptions || plan.uploads || plan.reorders) {
|
if (plan.deletions || plan.adoptions || plan.uploads || plan.reorders) {
|
||||||
// Structured
|
// Structured
|
||||||
let html = '<div style="text-align:left; max-height:400px; overflow-y:auto;">';
|
let html = '<div style="text-align:left; max-height:400px; overflow-y:auto;">';
|
||||||
html += renderSection('Deletions', plan.deletions, '🗑️');
|
html += renderSection('Deletions', plan.deletions, '🗑️', 'deletions');
|
||||||
html += renderSection('Adoptions (Save to Drive)', plan.adoptions, '⬇️');
|
html += renderSection('Adoptions (Save to Drive)', plan.adoptions, '⬇️', 'adoptions');
|
||||||
html += renderSection('Uploads (Send to Shopify)', plan.uploads, '⬆️');
|
html += renderSection('Uploads (Send to Shopify)', plan.uploads, '⬆️', 'uploads');
|
||||||
html += renderSection('Reorder & Rename', plan.reorders, '🔄');
|
html += renderSection('Reorder & Rename', plan.reorders, '🔄', 'reorders');
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -1914,16 +1923,8 @@
|
|||||||
// Build Plan Summary HTML
|
// Build Plan Summary HTML
|
||||||
let html = ui.renderPlanHtml(plan);
|
let html = ui.renderPlanHtml(plan);
|
||||||
|
|
||||||
// Progress Section (Hidden initially)
|
// Progress Section (Removed)
|
||||||
html += `
|
// html += `...`
|
||||||
<div id="execution-progress" style="margin-top:15px; display:none; border-top:1px solid #eee; padding-top:10px;">
|
|
||||||
<div id="prog-delete" style="color:#888;">🗑️ Deletions: Pending</div>
|
|
||||||
<div id="prog-adopt" style="color:#888;">⬇️ Adoptions: Pending</div>
|
|
||||||
<div id="prog-upload" style="color:#888;">⬆️ Uploads: Pending</div>
|
|
||||||
<div id="prog-reorder" style="color:#888;">🔄 Reorder: Pending</div>
|
|
||||||
<div id="prog-sheet" style="color:#888;">📊 Sheet Update: Pending</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('match-modal-text').innerHTML = html;
|
document.getElementById('match-modal-text').innerHTML = html;
|
||||||
|
|
||||||
@ -1959,165 +1960,48 @@
|
|||||||
const btnSkip = document.getElementById('btn-match-skip');
|
const btnSkip = document.getElementById('btn-match-skip');
|
||||||
btnConfirm.disabled = true;
|
btnConfirm.disabled = true;
|
||||||
btnSkip.disabled = true;
|
btnSkip.disabled = true;
|
||||||
|
btnConfirm.innerText = "Saving...";
|
||||||
|
|
||||||
document.getElementById('execution-progress').style.display = 'block';
|
btnConfirm.innerText = "Saving...";
|
||||||
|
|
||||||
|
// Removed progressDiv setup
|
||||||
|
|
||||||
// Job ID
|
// Job ID
|
||||||
const jobId = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
const jobId = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||||
this.startLogPolling(jobId);
|
this.startLogPolling(jobId);
|
||||||
|
|
||||||
// Helpers for UI
|
// Optimistic Sheet Update (Fire and Forget)
|
||||||
const updateStatus = (id, status, color) => {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) {
|
|
||||||
el.style.color = color;
|
|
||||||
if (status) el.innerText = el.innerText.split(':')[0] + ': ' + status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optimistic Sheet Update (Immediate Parallel Execution)
|
|
||||||
const pSheet = new Promise(resolve => {
|
|
||||||
const first = activeItems[0];
|
const first = activeItems[0];
|
||||||
let url = null;
|
let url = null;
|
||||||
if (first) {
|
if (first) {
|
||||||
// Determine best URL based on item type
|
|
||||||
// Note: MediaHandlers logic prioritizes Shopify URL if present.
|
|
||||||
// We mimic that logic here.
|
|
||||||
const isShopify = (first.source === 'shopify_only' || first.source === 'synced') && first.thumbnail;
|
const isShopify = (first.source === 'shopify_only' || first.source === 'synced') && first.thumbnail;
|
||||||
|
|
||||||
if (isShopify) {
|
if (isShopify) {
|
||||||
url = first.thumbnail;
|
url = first.thumbnail;
|
||||||
} else if (first.id) {
|
} else if (first.id) {
|
||||||
// Drive Item - Construct URL compatible with server logic
|
|
||||||
url = "https://drive.google.com/thumbnail?id=" + first.id + "&sz=w400";
|
url = "https://drive.google.com/thumbnail?id=" + first.id + "&sz=w400";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (url) {
|
||||||
if (!url) {
|
console.log("Triggering Optimistic Sheet Update");
|
||||||
updateStatus('prog-sheet', 'Skipped', '#aaa');
|
google.script.run.updateSpreadsheetThumbnail(state.sku, url);
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus('prog-sheet', 'Running...', 'blue');
|
// Full Backend Execution
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(() => {
|
.withSuccessHandler(() => {
|
||||||
updateStatus('prog-sheet', 'Done', 'green');
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.withFailureHandler(e => {
|
|
||||||
console.warn("Sheet update failed", e);
|
|
||||||
updateStatus('prog-sheet', 'Failed (Warn)', 'orange');
|
|
||||||
resolve(); // Don't block completion
|
|
||||||
})
|
|
||||||
.updateSpreadsheetThumbnail(state.sku, url);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Phase 1: Deletions
|
|
||||||
if (plan.deletions.length === 0) {
|
|
||||||
updateStatus('prog-delete', 'Skipped', '#aaa');
|
|
||||||
this.startParallelPhases(plan, activeItems, jobId, pSheet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus('prog-delete', 'Running...', 'blue');
|
|
||||||
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(() => {
|
|
||||||
updateStatus('prog-delete', 'Done', 'green');
|
|
||||||
this.startParallelPhases(plan, activeItems, jobId, pSheet);
|
|
||||||
})
|
|
||||||
.withFailureHandler(e => {
|
|
||||||
updateStatus('prog-delete', 'Failed', 'red');
|
|
||||||
alert("Deletion Phase Failed: " + e.message);
|
|
||||||
this.stopLogPolling();
|
|
||||||
ui.setSavingState(false);
|
|
||||||
})
|
|
||||||
.executeSavePhase(state.sku, 'deletions', plan.deletions, jobId);
|
|
||||||
},
|
|
||||||
|
|
||||||
startParallelPhases(plan, activeItems, jobId, pSheet) {
|
|
||||||
// Helper function defined at the top to avoid hoisting issues
|
|
||||||
const updateStatus = (id, status, color) => {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) {
|
|
||||||
el.style.color = color;
|
|
||||||
if (status) el.innerText = el.innerText.split(':')[0] + ': ' + status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Phase 2 & 3: Adoptions & Uploads (Parallel)
|
|
||||||
const pAdoption = new Promise((resolve, reject) => {
|
|
||||||
if (plan.adoptions.length === 0) {
|
|
||||||
updateStatus('prog-adopt', 'Skipped', '#aaa');
|
|
||||||
return resolve();
|
|
||||||
}
|
|
||||||
updateStatus('prog-adopt', 'Running...', 'blue');
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(() => {
|
|
||||||
updateStatus('prog-adopt', 'Done', 'green');
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.withFailureHandler(reject)
|
|
||||||
.executeSavePhase(state.sku, 'adoptions', plan.adoptions, jobId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const pUpload = new Promise((resolve, reject) => {
|
|
||||||
if (plan.uploads.length === 0) {
|
|
||||||
updateStatus('prog-upload', 'Skipped', '#aaa');
|
|
||||||
return resolve();
|
|
||||||
}
|
|
||||||
updateStatus('prog-upload', 'Running...', 'blue');
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(() => {
|
|
||||||
updateStatus('prog-upload', 'Done', 'green');
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.withFailureHandler(reject)
|
|
||||||
.executeSavePhase(state.sku, 'uploads', plan.uploads, jobId);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([pAdoption, pUpload])
|
|
||||||
.then(() => {
|
|
||||||
// Phase 4: Reorder
|
|
||||||
const pReorder = new Promise((resolve, reject) => {
|
|
||||||
if (plan.reorders.length === 0) {
|
|
||||||
updateStatus('prog-reorder', 'Skipped', '#aaa');
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus('prog-reorder', 'Running...', 'blue');
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(() => {
|
|
||||||
updateStatus('prog-reorder', 'Done', 'green');
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.withFailureHandler(e => {
|
|
||||||
alert("Reorder Phase Failed: " + e.message);
|
|
||||||
reject(e);
|
|
||||||
})
|
|
||||||
.executeSavePhase(state.sku, 'reorder', plan.reorders, jobId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for BOTH Reorder and Early Sheet Update
|
|
||||||
Promise.all([pReorder, pSheet])
|
|
||||||
.then(() => {
|
|
||||||
this.finishSave(true);
|
this.finishSave(true);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.withFailureHandler((e) => {
|
||||||
// If reorder failed, we already alerted.
|
console.error("Save Failed", e);
|
||||||
console.error("Save completion failed", e);
|
// progressDiv.innerHTML += ... removed
|
||||||
|
alert("Save Operation Failed: " + e.message);
|
||||||
this.finishSave(false);
|
this.finishSave(false);
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.executeFullSavePlan(state.sku, plan, jobId);
|
||||||
alert("Parallel Execution Failed: " + e.message);
|
|
||||||
this.stopLogPolling();
|
|
||||||
ui.setSavingState(false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
finishSave(success) {
|
finishSave(success) {
|
||||||
this.stopLogPolling();
|
this.stopLogPolling();
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -2148,7 +2032,29 @@
|
|||||||
// Simple approach: standard loop since we know count
|
// Simple approach: standard loop since we know count
|
||||||
if (logs.length > this.knownLogCount) {
|
if (logs.length > this.knownLogCount) {
|
||||||
const newLogs = logs.slice(this.knownLogCount);
|
const newLogs = logs.slice(this.knownLogCount);
|
||||||
newLogs.forEach(l => ui.logStatus('stream', l));
|
|
||||||
|
newLogs.forEach(l => {
|
||||||
|
// Check for Signal
|
||||||
|
if (l.startsWith('[SIGNAL] ')) {
|
||||||
|
try {
|
||||||
|
var sig = JSON.parse(l.substring(9));
|
||||||
|
// Format: { phase: string, id: string, status: string }
|
||||||
|
var li = document.getElementById('li-plan-' + sig.phase + '-' + sig.id);
|
||||||
|
if (li) {
|
||||||
|
li.style.color = '#aaa';
|
||||||
|
li.style.textDecoration = 'line-through';
|
||||||
|
var statusSpan = li.querySelector('.plan-status');
|
||||||
|
if (statusSpan) statusSpan.innerText = '✅';
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn("Signal Parse Error", e); }
|
||||||
|
return; // Do NOT log signal to UI
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.logStatus('stream', l);
|
||||||
|
|
||||||
|
// Modal progress update removed
|
||||||
|
});
|
||||||
|
|
||||||
this.knownLogCount = logs.length;
|
this.knownLogCount = logs.length;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
|
|||||||
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
||||||
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
||||||
import { installSalesSyncTrigger } from "./triggers"
|
import { installSalesSyncTrigger } from "./triggers"
|
||||||
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia, pollJobLogs, getMediaManagerInitialState, getMediaSavePlan, executeSavePhase, updateSpreadsheetThumbnail } from "./mediaHandlers"
|
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia, pollJobLogs, getMediaManagerInitialState, getMediaSavePlan, executeSavePhase, updateSpreadsheetThumbnail, executeFullSavePlan } from "./mediaHandlers"
|
||||||
import { runSystemDiagnostics } from "./verificationSuite"
|
import { runSystemDiagnostics } from "./verificationSuite"
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -70,3 +70,4 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
|||||||
;(global as any).getMediaSavePlan = getMediaSavePlan
|
;(global as any).getMediaSavePlan = getMediaSavePlan
|
||||||
;(global as any).executeSavePhase = executeSavePhase
|
;(global as any).executeSavePhase = executeSavePhase
|
||||||
;(global as any).updateSpreadsheetThumbnail = updateSpreadsheetThumbnail
|
;(global as any).updateSpreadsheetThumbnail = updateSpreadsheetThumbnail
|
||||||
|
;(global as any).executeFullSavePlan = executeFullSavePlan
|
||||||
|
|||||||
@ -66,6 +66,14 @@ export function getPickerConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function fetchRawData(sku: string) {
|
||||||
|
// expose for testing if needed, or if UI needs raw dump
|
||||||
|
// but MediaService implementation is private.
|
||||||
|
// We stick to getInitialState.
|
||||||
|
}
|
||||||
|
|
||||||
export function getMediaForSku(sku: string): any[] {
|
export function getMediaForSku(sku: string): any[] {
|
||||||
const config = new Config()
|
const config = new Config()
|
||||||
const driveService = new GASDriveService()
|
const driveService = new GASDriveService()
|
||||||
@ -157,7 +165,7 @@ export function updateSpreadsheetThumbnail(sku: string, forcedThumbnailUrl: stri
|
|||||||
|
|
||||||
// Need Shopify ID for accurate state logic?
|
// Need Shopify ID for accurate state logic?
|
||||||
// getUnifiedMediaState uses it.
|
// getUnifiedMediaState uses it.
|
||||||
try { product.MatchToShopifyProduct(shop); } catch(e) {}
|
try { product.MatchToShopifyProduct(shop); } catch(e) { /* ignore mismatch during initial load */ }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Refresh state to get Shopify CDN URLs
|
// Refresh state to get Shopify CDN URLs
|
||||||
@ -244,6 +252,28 @@ export function executeSavePhase(sku: string, phase: string, planData: any, jobI
|
|||||||
return mediaService.executeSavePhase(sku, phase, planData, product.shopify_id, jobId);
|
return mediaService.executeSavePhase(sku, phase, planData, product.shopify_id, jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function executeFullSavePlan(sku: string, plan: any, jobId: string | null = null) {
|
||||||
|
const config = new Config()
|
||||||
|
const driveService = new GASDriveService()
|
||||||
|
const shop = new Shop()
|
||||||
|
const shopifyMediaService = new ShopifyMediaService(shop)
|
||||||
|
const networkService = new GASNetworkService()
|
||||||
|
const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config)
|
||||||
|
|
||||||
|
const product = new Product(sku)
|
||||||
|
try {
|
||||||
|
product.MatchToShopifyProduct(shop);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("MatchToShopifyProduct failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product.shopify_id) {
|
||||||
|
throw new Error("Product must be synced to Shopify before saving media changes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaService.executeFullSavePlan(sku, plan, product.shopify_id, jobId);
|
||||||
|
}
|
||||||
|
|
||||||
export function pollJobLogs(jobId: string): string[] {
|
export function pollJobLogs(jobId: string): string[] {
|
||||||
try {
|
try {
|
||||||
const cache = CacheService.getDocumentCache();
|
const cache = CacheService.getDocumentCache();
|
||||||
|
|||||||
@ -130,7 +130,7 @@ describe("MediaService V2 Integration Logic", () => {
|
|||||||
getThumbnail: () => ({ getBytes: () => [] }),
|
getThumbnail: () => ({ getBytes: () => [] }),
|
||||||
getMimeType: () => "image/jpeg"
|
getMimeType: () => "image/jpeg"
|
||||||
}
|
}
|
||||||
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1" })
|
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", getUrl: () => "http://mock.url" })
|
||||||
mockDrive.getFiles.mockReturnValue([driveFile])
|
mockDrive.getFiles.mockReturnValue([driveFile])
|
||||||
|
|
||||||
// Setup Shopify
|
// Setup Shopify
|
||||||
@ -160,7 +160,7 @@ describe("MediaService V2 Integration Logic", () => {
|
|||||||
getThumbnail: () => ({ getBytes: () => [] }),
|
getThumbnail: () => ({ getBytes: () => [] }),
|
||||||
getMimeType: () => "image/jpeg"
|
getMimeType: () => "image/jpeg"
|
||||||
}
|
}
|
||||||
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1" })
|
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", getUrl: () => "http://mock.url" })
|
||||||
mockDrive.getFiles.mockReturnValue([driveFile])
|
mockDrive.getFiles.mockReturnValue([driveFile])
|
||||||
mockShopify.getProductMedia.mockReturnValue([])
|
mockShopify.getProductMedia.mockReturnValue([])
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ describe("MediaService V2 Integration Logic", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("should identify Shopify-Only items", () => {
|
test("should identify Shopify-Only items", () => {
|
||||||
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", addFile: jest.fn() })
|
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", getUrl: () => "http://mock.url", addFile: jest.fn() })
|
||||||
mockDrive.getFiles.mockReturnValue([])
|
mockDrive.getFiles.mockReturnValue([])
|
||||||
|
|
||||||
const shopMedia = {
|
const shopMedia = {
|
||||||
@ -230,7 +230,7 @@ describe("MediaService V2 Integration Logic", () => {
|
|||||||
jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([])
|
jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([])
|
||||||
|
|
||||||
// Mock file creation
|
// Mock file creation
|
||||||
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", addFile: jest.fn() })
|
mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", getUrl: () => "http://mock.url", addFile: jest.fn() })
|
||||||
// We set default mockDrive.createFile above but we can specialize if needed
|
// We set default mockDrive.createFile above but we can specialize if needed
|
||||||
// Default returns "new_created_file_id"
|
// Default returns "new_created_file_id"
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,36 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDiagnostics(sku: string, shopifyProductId: string) {
|
private fetchRawData(sku: string, shopifyProductId: string) {
|
||||||
|
const result = {
|
||||||
|
drive: { folder: null, files: [], error: null, folderUrl: null },
|
||||||
|
shopify: { media: [], error: null }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Unsafe Drive Check
|
||||||
|
try {
|
||||||
|
const folder = this.driveService.getOrCreateFolder(sku, this.config.productPhotosFolderId);
|
||||||
|
result.drive.folder = folder;
|
||||||
|
result.drive.folderUrl = folder.getUrl();
|
||||||
|
// Fetch files with properties immediately
|
||||||
|
result.drive.files = this.driveService.getFilesWithProperties(folder.getId());
|
||||||
|
} catch (e) {
|
||||||
|
result.drive.error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Unsafe Shopify Check
|
||||||
|
if (shopifyProductId) {
|
||||||
|
try {
|
||||||
|
result.shopify.media = this.shopifyMediaService.getProductMedia(shopifyProductId);
|
||||||
|
} catch (e) {
|
||||||
|
result.shopify.error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiagnostics(sku: string, shopifyProductId: string, rawData?: any) {
|
||||||
const results = {
|
const results = {
|
||||||
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
||||||
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, error: null },
|
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, error: null },
|
||||||
@ -58,55 +87,58 @@ export class MediaService {
|
|||||||
console.warn("Failed to check active job", e);
|
console.warn("Failed to check active job", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Unsafe Drive Check
|
// Ensure we have data
|
||||||
try {
|
const data = rawData || this.fetchRawData(sku, shopifyProductId);
|
||||||
const folder = this.driveService.getOrCreateFolder(sku, this.config.productPhotosFolderId)
|
|
||||||
results.drive.folderId = folder.getId()
|
|
||||||
results.drive.folderUrl = folder.getUrl()
|
|
||||||
const files = this.driveService.getFiles(folder.getId())
|
|
||||||
results.drive.fileCount = files.length
|
|
||||||
results.drive.status = 'ok'
|
|
||||||
} catch (e) {
|
|
||||||
results.drive.status = 'error'
|
|
||||||
results.drive.error = e.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Unsafe Shopify Check
|
// 1. Drive Status
|
||||||
try {
|
if (data.drive.error) {
|
||||||
if (shopifyProductId) {
|
results.drive.status = 'error';
|
||||||
const media = this.shopifyMediaService.getProductMedia(shopifyProductId)
|
results.drive.error = data.drive.error.toString();
|
||||||
results.shopify.mediaCount = media.length
|
|
||||||
// Admin URL construction (Best effort)
|
|
||||||
// Assuming standard Shopify admin pattern
|
|
||||||
const domain = this.shopifyMediaService.getShopDomain? this.shopifyMediaService.getShopDomain() : 'admin.shopify.com';
|
|
||||||
results.shopify.adminUrl = `https://${domain.replace('.myshopify.com','')}.myshopify.com/admin/products/${shopifyProductId.split('/').pop()}`
|
|
||||||
results.shopify.status = 'ok'
|
|
||||||
} else {
|
} else {
|
||||||
results.shopify.status = 'skipped' // Not linked yet
|
results.drive.folderId = data.drive.folder ? data.drive.folder.getId() : null;
|
||||||
}
|
results.drive.folderUrl = data.drive.folderUrl;
|
||||||
} catch (e) {
|
results.drive.fileCount = data.drive.files.length;
|
||||||
results.shopify.status = 'error'
|
results.drive.status = 'ok';
|
||||||
results.shopify.error = e.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
// 2. Shopify Status
|
||||||
|
if (shopifyProductId) {
|
||||||
|
if (data.shopify.error) {
|
||||||
|
results.shopify.status = 'error';
|
||||||
|
results.shopify.error = data.shopify.error.toString();
|
||||||
|
} else {
|
||||||
|
results.shopify.mediaCount = data.shopify.media.length;
|
||||||
|
// Admin URL construction (Best effort)
|
||||||
|
const domain = this.shopifyMediaService.getShopDomain ? this.shopifyMediaService.getShopDomain() : 'admin.shopify.com';
|
||||||
|
results.shopify.adminUrl = `https://${domain.replace('.myshopify.com', '')}.myshopify.com/admin/products/${shopifyProductId.split('/').pop()}`;
|
||||||
|
results.shopify.status = 'ok';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.shopify.status = 'skipped';
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnifiedMediaState(sku: string, shopifyProductId: string): any[] {
|
return results;
|
||||||
console.log(`MediaService: Getting unified state for SKU ${sku}`)
|
}
|
||||||
|
|
||||||
|
getUnifiedMediaState(sku: string, shopifyProductId: string, rawData?: any): any[] {
|
||||||
|
console.log(`MediaService: Getting unified state for SKU ${sku}`);
|
||||||
|
|
||||||
|
const data = rawData || this.fetchRawData(sku, shopifyProductId);
|
||||||
|
|
||||||
|
// Handle Errors from Fetch
|
||||||
|
if (data.drive.error) {
|
||||||
|
console.warn("Drive fetch failed, returning empty state or throwing?", data.drive.error);
|
||||||
|
// Previously we let it crash or return partial. Let's return empty if drive fails as it's the primary source.
|
||||||
|
return [];
|
||||||
|
// OR: throw data.drive.error; // To match previous behavior?
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Get Drive Files
|
// 1. Get Drive Files
|
||||||
const folder = this.driveService.getOrCreateFolder(sku, this.config.productPhotosFolderId)
|
// const folder = ... // Already in data.drive.folder
|
||||||
// We need strict file list.
|
const driveFiles = data.drive.files;
|
||||||
// Optimization: getFiles() usually returns limited info.
|
|
||||||
// We might need to iterate and pull props if getFiles() doesn't include appProperties (DriveApp doesn't).
|
|
||||||
const driveFiles = this.driveService.getFilesWithProperties(folder.getId())
|
|
||||||
|
|
||||||
// 2. Get Shopify Media
|
// 2. Get Shopify Media
|
||||||
let shopifyMedia: any[] = []
|
let shopifyMedia = data.shopify.media || [];
|
||||||
if (shopifyProductId) {
|
|
||||||
shopifyMedia = this.shopifyMediaService.getProductMedia(shopifyProductId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Match
|
// 3. Match
|
||||||
const unifiedState: any[] = []
|
const unifiedState: any[] = []
|
||||||
@ -156,12 +188,37 @@ export class MediaService {
|
|||||||
// Sort: Gallery Order ASC, then Filename ASC
|
// Sort: Gallery Order ASC, then Filename ASC
|
||||||
driveFileStats.sort((a, b) => {
|
driveFileStats.sort((a, b) => {
|
||||||
if (a.galleryOrder !== b.galleryOrder) {
|
if (a.galleryOrder !== b.galleryOrder) {
|
||||||
return a.galleryOrder - b.galleryOrder
|
return a.galleryOrder - b.galleryOrder;
|
||||||
}
|
}
|
||||||
return a.file.getName().localeCompare(b.file.getName())
|
return a.file.getName().localeCompare(b.file.getName());
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Batch Status Check for Videos with Sidecars
|
||||||
|
const videoStatusMap = new Map<string, any>();
|
||||||
|
// Identify videos that MIGHT be ready (have sidecar)
|
||||||
|
const videosToCheck = driveFileStats.filter(d => sidecarThumbMap.has(d.file.getId()));
|
||||||
|
|
||||||
|
if (videosToCheck.length > 0 && typeof Drive !== 'undefined') {
|
||||||
|
try {
|
||||||
|
// Check status for ALL videos in folder. Easier than filtering by specific IDs in 'q' which has length limits.
|
||||||
|
// We assume the folder ID is valid.
|
||||||
|
const folderId = data.drive.folder ? data.drive.folder.getId() : null;
|
||||||
|
if (folderId) {
|
||||||
|
// @ts-ignore
|
||||||
|
const response = Drive.Files.list({
|
||||||
|
q: `'${folderId}' in parents and mimeType contains 'video/' and trashed = false`,
|
||||||
|
fields: 'files(id, hasThumbnail, thumbnailLink, videoMediaMetadata)'
|
||||||
|
});
|
||||||
|
if (response.files) {
|
||||||
|
response.files.forEach((f: any) => videoStatusMap.set(f.id, f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[MediaService] Batch video status check failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match Logic (Strict ID Match Only)
|
// Match Logic (Strict ID Match Only)
|
||||||
driveFileStats.forEach(d => {
|
driveFileStats.forEach(d => {
|
||||||
// Skip Sidecar Files in main list
|
// Skip Sidecar Files in main list
|
||||||
@ -203,44 +260,22 @@ export class MediaService {
|
|||||||
// But we want to CLEANUP.
|
// But we want to CLEANUP.
|
||||||
// Let's use Advanced Drive API to check `thumbnailLink` existence for this specific file, if it has a sidecar.
|
// Let's use Advanced Drive API to check `thumbnailLink` existence for this specific file, if it has a sidecar.
|
||||||
// This minimizes API calls to ONLY when we have a sidecar candidate.
|
// This minimizes API calls to ONLY when we have a sidecar candidate.
|
||||||
if (sidecarThumbMap.has(d.file.getId())) {
|
// Batch Optimized Check
|
||||||
const fileId = d.file.getId();
|
if (videoStatusMap.has(d.file.getId())) {
|
||||||
// @ts-ignore
|
const meta = videoStatusMap.get(d.file.getId());
|
||||||
const drive = Drive;
|
|
||||||
const meta = drive.Files.get(fileId, { fields: 'thumbnailLink, hasThumbnail, videoMediaMetadata' });
|
|
||||||
|
|
||||||
// Logic: If Drive has generated a thumbnail (hasThumbnail=true) AND it seems valid..
|
// Logic: If Drive has generated a thumbnail (hasThumbnail=true) AND it seems valid..
|
||||||
// Note: Drive sets hasThumbnail=true even for generic icons sometimes?
|
// Check `videoMediaMetadata.width` to ensure processing is complete (width is often missing during processing)
|
||||||
// But `thumbnailLink` definitely exists.
|
|
||||||
// For videos, `videoMediaMetadata` might NOT have 'width' while processing?
|
|
||||||
// Let's check `videoMediaMetadata.width`.
|
|
||||||
if (meta.thumbnailLink && meta.videoMediaMetadata && meta.videoMediaMetadata.width) {
|
if (meta.thumbnailLink && meta.videoMediaMetadata && meta.videoMediaMetadata.width) {
|
||||||
// SUCCESS: Drive has finished processing (we have dimensions).
|
// SUCCESS: Drive has finished processing.
|
||||||
nativeThumbReady = true;
|
nativeThumbReady = true;
|
||||||
// We don't construct the URL here, we let the standard logic below handle it?
|
|
||||||
// No, we need the bytes for the frontend or a link.
|
|
||||||
// `thumbnailLink` is short lived.
|
|
||||||
// Let's use the native generation below.
|
|
||||||
console.log(`[MediaService] Video ${d.file.getName()} finished processing. Cleaning sidecar.`);
|
console.log(`[MediaService] Video ${d.file.getName()} finished processing. Cleaning sidecar.`);
|
||||||
|
|
||||||
// Cleanup Sidecar Loop
|
// Cleanup Sidecar
|
||||||
// TRASH the sidecar file.
|
const sidecarId = d.customThumbnailId; // Direct lookup from properties
|
||||||
// We need the sidecar ID. We have to map IDs or iterate.
|
|
||||||
// Optimization: We didn't store Sidecar ID in the simpler Map.
|
|
||||||
// Let's find it.
|
|
||||||
const sidecarId = Array.from(sidecarFileIds).find(id => {
|
|
||||||
// This is slow: O(N) lookup.
|
|
||||||
// But we only do this ONCE per file lifecycle.
|
|
||||||
// Actually better to store ID in map?
|
|
||||||
// Let's just find the file in `driveFiles` that corresponds.
|
|
||||||
// We have `d.customThumbnailId`!
|
|
||||||
return id === d.customThumbnailId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sidecarId) {
|
if (sidecarId) {
|
||||||
try {
|
try {
|
||||||
this.driveService.trashFile(sidecarId);
|
this.driveService.trashFile(sidecarId);
|
||||||
sidecarFileIds.delete(sidecarId); // Remove from set so we don't trip later
|
sidecarFileIds.delete(sidecarId);
|
||||||
sidecarThumbMap.delete(d.file.getId());
|
sidecarThumbMap.delete(d.file.getId());
|
||||||
console.log(`[MediaService] Trashed sidecar ${sidecarId}`);
|
console.log(`[MediaService] Trashed sidecar ${sidecarId}`);
|
||||||
} catch (trashErr) {
|
} catch (trashErr) {
|
||||||
@ -251,7 +286,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
// Ignore individual file errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Check Sidecar (If it still exists after potential cleanup)
|
// 1. Check Sidecar (If it still exists after potential cleanup)
|
||||||
@ -272,7 +307,7 @@ export class MediaService {
|
|||||||
try {
|
try {
|
||||||
const nativeThumb = `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`;
|
const nativeThumb = `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`;
|
||||||
if (nativeThumb.length > 100) thumbnail = nativeThumb;
|
if (nativeThumb.length > 100) thumbnail = nativeThumb;
|
||||||
} catch(e) {}
|
} catch(e) { /* ignore thumbnail generation error */ }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 2. Native / Fallback
|
// 2. Native / Fallback
|
||||||
@ -506,11 +541,15 @@ export class MediaService {
|
|||||||
if (item.driveId) {
|
if (item.driveId) {
|
||||||
try {
|
try {
|
||||||
if (item.customThumbnailId) {
|
if (item.customThumbnailId) {
|
||||||
try { this.driveService.trashFile(item.customThumbnailId); } catch(e) {}
|
try { this.driveService.trashFile(item.customThumbnailId); } catch(e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
this.driveService.trashFile(item.driveId);
|
this.driveService.trashFile(item.driveId);
|
||||||
log(`- Trashed in Drive (${item.driveId})`);
|
log(`- Trashed in Drive (${item.driveId})`);
|
||||||
|
log(`[SIGNAL] {"phase": "deletions", "id": "${item.id}", "status": "complete"}`);
|
||||||
} catch (e) { log(`- Failed to delete from Drive: ${e.message}`); }
|
} catch (e) { log(`- Failed to delete from Drive: ${e.message}`); }
|
||||||
|
} else if (item.shopifyId && !item.driveId) {
|
||||||
|
// Shopify Only deletion
|
||||||
|
log(`[SIGNAL] {"phase": "deletions", "id": "${item.id}", "status": "complete"}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -554,6 +593,7 @@ export class MediaService {
|
|||||||
this.driveService.updateFileProperties(file.getId(), { shopify_media_id: item.shopifyId });
|
this.driveService.updateFileProperties(file.getId(), { shopify_media_id: item.shopifyId });
|
||||||
|
|
||||||
log(`- Adopted ${item.filename} => Drive ID: ${file.getId()}`);
|
log(`- Adopted ${item.filename} => Drive ID: ${file.getId()}`);
|
||||||
|
log(`[SIGNAL] {"phase": "adoptions", "id": "${item.id}", "status": "complete"}`);
|
||||||
} else {
|
} else {
|
||||||
log(`- Failed to download ${item.filename}`);
|
log(`- Failed to download ${item.filename}`);
|
||||||
}
|
}
|
||||||
@ -653,6 +693,7 @@ export class MediaService {
|
|||||||
item.source = 'synced';
|
item.source = 'synced';
|
||||||
this.driveService.updateFileProperties(item.driveId, { shopify_media_id: created.id });
|
this.driveService.updateFileProperties(item.driveId, { shopify_media_id: created.id });
|
||||||
log(`- Created in Shopify (${created.id})`);
|
log(`- Created in Shopify (${created.id})`);
|
||||||
|
log(`[SIGNAL] {"phase": "uploads", "id": "${item.id}", "status": "complete"}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -691,6 +732,8 @@ export class MediaService {
|
|||||||
log(`- Renamed ${currentName} -> ${newName}`);
|
log(`- Renamed ${currentName} -> ${newName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`[SIGNAL] {"phase": "reorders", "id": "${item.id}", "status": "complete"}`);
|
||||||
|
|
||||||
// C. Prepare Shopify Reorder
|
// C. Prepare Shopify Reorder
|
||||||
if (item.shopifyId) {
|
if (item.shopifyId) {
|
||||||
reorderMoves.push({ id: item.shopifyId, newPosition: index.toString() });
|
reorderMoves.push({ id: item.shopifyId, newPosition: index.toString() });
|
||||||
@ -712,19 +755,81 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInitialState(sku: string, shopifyProductId: string): { diagnostics: any, media: any[] } {
|
getInitialState(sku: string, shopifyProductId: string): { diagnostics: any, media: any[] } {
|
||||||
// 1. Diagnostics (Reusing the existing method logic but avoiding redundant setup)
|
// New Implementation using Fetch Once
|
||||||
const diagnostics = this.getDiagnostics(sku, shopifyProductId);
|
const rawData = this.fetchRawData(sku, shopifyProductId);
|
||||||
|
const diagnostics = this.getDiagnostics(sku, shopifyProductId, rawData);
|
||||||
// 2. Unified Media State
|
const media = this.getUnifiedMediaState(sku, shopifyProductId, rawData);
|
||||||
// If diagnostics succeeded in finding the folder, we should probably pass that info
|
|
||||||
// to getUnifiedMediaState to avoid re-fetching the folder, but for now
|
|
||||||
// let's just call the method to keep it clean.
|
|
||||||
const media = this.getUnifiedMediaState(sku, shopifyProductId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
diagnostics,
|
diagnostics,
|
||||||
media
|
media
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeFullSavePlan(sku: string, plan: any, shopifyProductId: string, jobId: string | null = null): string[] {
|
||||||
|
const logs: string[] = [];
|
||||||
|
const log = (msg: string) => {
|
||||||
|
logs.push(msg);
|
||||||
|
console.log(msg);
|
||||||
|
if (jobId) this.logToCache(jobId, msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
log(`Starting Save Operation for SKU ${sku}`);
|
||||||
|
|
||||||
|
// Store Active Job ID
|
||||||
|
if (jobId) {
|
||||||
|
CacheService.getDocumentCache().put(`active_job_${sku}`, jobId, 600); // 10 min lock
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Deletions
|
||||||
|
if (plan.deletions && plan.deletions.length > 0) {
|
||||||
|
log(`Phase 1/4: Executing ${plan.deletions.length} Deletions...`);
|
||||||
|
this.executeDeletions(plan.deletions, shopifyProductId, log);
|
||||||
|
} else {
|
||||||
|
log('Phase 1/4: No Deletions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Adoptions
|
||||||
|
if (plan.adoptions && plan.adoptions.length > 0) {
|
||||||
|
log(`Phase 2/4: Executing ${plan.adoptions.length} Adoptions...`);
|
||||||
|
this.executeAdoptions(sku, plan.adoptions, log);
|
||||||
|
} else {
|
||||||
|
log('Phase 2/4: No Adoptions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Uploads
|
||||||
|
if (plan.uploads && plan.uploads.length > 0) {
|
||||||
|
log(`Phase 3/4: Executing ${plan.uploads.length} Uploads...`);
|
||||||
|
this.executeUploads(sku, plan.uploads, shopifyProductId, log);
|
||||||
|
} else {
|
||||||
|
log('Phase 3/4: No Uploads.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Reorder & Rename
|
||||||
|
if (plan.reorders && plan.reorders.length > 0) {
|
||||||
|
log(`Phase 4/4: Executing Reorder & Rename...`);
|
||||||
|
this.executeReorderAndRename(sku, plan.reorders, shopifyProductId, log);
|
||||||
|
} else {
|
||||||
|
log('Phase 4/4: No Reordering.');
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Save Operation Completed Successfully.");
|
||||||
|
|
||||||
|
// Clear Job Lock
|
||||||
|
if (jobId) {
|
||||||
|
CacheService.getDocumentCache().remove(`active_job_${sku}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log(`CRITICAL ERROR: Save failed: ${e.message}`);
|
||||||
|
// Clear Job Lock on error too so user isn't stuck forever
|
||||||
|
if (jobId) {
|
||||||
|
CacheService.getDocumentCache().remove(`active_job_${sku}`);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,59 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
describe('Global Function Exports', () => {
|
describe('Global Function Exports (AST Analysis)', () => {
|
||||||
const srcDir = path.resolve(__dirname, '../'); // Assumes src/test/GlobalFunctions.test.ts
|
const srcDir = path.resolve(__dirname, '../');
|
||||||
const globalFile = path.join(srcDir, 'global.ts');
|
const globalFile = path.join(srcDir, 'global.ts');
|
||||||
|
|
||||||
// 1. Get all globally exported function names
|
// --- Helper: Parse Global Exports ---
|
||||||
const getGlobalExports = (): Set<string> => {
|
const getGlobalExports = (): Set<string> => {
|
||||||
const content = fs.readFileSync(globalFile, 'utf-8');
|
const content = fs.readFileSync(globalFile, 'utf-8');
|
||||||
const regex = /;\(global as any\)\.(\w+)\s*=/g;
|
const sourceFile = ts.createSourceFile('global.ts', content, ts.ScriptTarget.Latest, true);
|
||||||
const exports = new Set<string>();
|
const exports = new Set<string>();
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(content)) !== null) {
|
const visit = (node: ts.Node) => {
|
||||||
exports.add(match[1]);
|
// Look for: ;(global as any).funcName = ...
|
||||||
|
if (ts.isBinaryExpression(node) &&
|
||||||
|
node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||||
|
|
||||||
|
let left = node.left;
|
||||||
|
|
||||||
|
// Handle property access: (exp).funcName or exp.funcName
|
||||||
|
if (ts.isPropertyAccessExpression(left)) {
|
||||||
|
|
||||||
|
// Check if expression is (global as any) or global
|
||||||
|
let expression: ts.Expression = left.expression;
|
||||||
|
|
||||||
|
// Unprap parens: ((global as any))
|
||||||
|
while (ts.isParenthesizedExpression(expression)) {
|
||||||
|
expression = expression.expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap 'as': global as any
|
||||||
|
if (ts.isAsExpression(expression)) {
|
||||||
|
expression = expression.expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isIdentifier(expression) && expression.text === 'global') {
|
||||||
|
if (ts.isIdentifier(left.name)) {
|
||||||
|
exports.add(left.name.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(sourceFile);
|
||||||
return exports;
|
return exports;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Find all google.script.run calls in HTML files
|
// --- Helper: Find google.script.run Calls ---
|
||||||
const getFrontendCalls = (): Map<string, string> => {
|
const getFrontendCalls = (): Map<string, string> => {
|
||||||
const calls = new Map<string, string>(); // functionName -> filename (for error msg)
|
const calls = new Map<string, string>(); // functionName -> filename
|
||||||
|
|
||||||
const scanDir = (dir: string) => {
|
const scanDir = (dir: string) => {
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
@ -30,28 +64,31 @@ describe('Global Function Exports', () => {
|
|||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
scanDir(fullPath);
|
scanDir(fullPath);
|
||||||
} else if (file.endsWith('.html')) {
|
} else if (file.endsWith('.html')) {
|
||||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
const htmlContent = fs.readFileSync(fullPath, 'utf-8');
|
||||||
// Matches:
|
const $ = cheerio.load(htmlContent);
|
||||||
// google.script.run.myFunc()
|
|
||||||
// google.script.run.withSuccessHandler(...).myFunc()
|
|
||||||
// google.script.run.withFailureHandler(...).myFunc()
|
|
||||||
// google.script.run.withSuccessHandler(...).withFailureHandler(...).myFunc()
|
|
||||||
|
|
||||||
// Regex strategy:
|
$('script').each((_, script) => {
|
||||||
// 1. Find "google.script.run"
|
const scriptContent = $(script).html();
|
||||||
// 2. Consume optional handlers .with...(...)
|
if (!scriptContent) return;
|
||||||
// 3. Capture the final function name .FunctionName(
|
|
||||||
|
|
||||||
const callRegex = /google\.script\.run(?:[\s\n]*\.(?:withSuccessHandler|withFailureHandler|withUserObject)\([^)]*\))*[\s\n]*\.(\w+)\s*\(/g;
|
const sourceFile = ts.createSourceFile(file + '.js', scriptContent, ts.ScriptTarget.Latest, true);
|
||||||
|
|
||||||
let match;
|
const visit = (node: ts.Node) => {
|
||||||
while ((match = callRegex.exec(content)) !== null) {
|
if (ts.isCallExpression(node)) {
|
||||||
const funcName = match[1];
|
// Check if this call is part of a google.script.run chain
|
||||||
if (!['withSuccessHandler', 'withFailureHandler', 'withUserObject'].includes(funcName)) {
|
const chain = analyzeChain(node.expression);
|
||||||
calls.set(funcName, file);
|
if (chain && chain.isGoogleScriptRun) {
|
||||||
|
if (!['withSuccessHandler', 'withFailureHandler', 'withUserObject'].includes(chain.methodName)) {
|
||||||
|
calls.set(chain.methodName, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ts.forEachChild(node, visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(sourceFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,10 +96,53 @@ describe('Global Function Exports', () => {
|
|||||||
return calls;
|
return calls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to analyze property access chain
|
||||||
|
// Returns { isGoogleScriptRun: boolean, methodName: string } if valid
|
||||||
|
const analyzeChain = (expression: ts.Expression): { isGoogleScriptRun: boolean, methodName: string } | null => {
|
||||||
|
if (!ts.isPropertyAccessExpression(expression)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ts.isIdentifier(expression.name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodName = expression.name.text;
|
||||||
|
let current = expression.expression;
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let p = current;
|
||||||
|
|
||||||
|
while (depth < 20) { // Safety break
|
||||||
|
if (ts.isCallExpression(p)) {
|
||||||
|
p = p.expression;
|
||||||
|
} else if (ts.isPropertyAccessExpression(p)) {
|
||||||
|
// Check for google.script.run
|
||||||
|
if (ts.isIdentifier(p.name) && p.name.text === 'run') {
|
||||||
|
// check exp.exp is script, exp.exp.exp is google
|
||||||
|
if (ts.isPropertyAccessExpression(p.expression) &&
|
||||||
|
ts.isIdentifier(p.expression.name) &&
|
||||||
|
p.expression.name.text === 'script' &&
|
||||||
|
ts.isIdentifier(p.expression.expression) &&
|
||||||
|
p.expression.expression.text === 'google') {
|
||||||
|
return { isGoogleScriptRun: true, methodName };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p = p.expression;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
test('All client-side google.script.run calls must be exported in global.ts', () => {
|
test('All client-side google.script.run calls must be exported in global.ts', () => {
|
||||||
const globalExports = getGlobalExports();
|
const globalExports = getGlobalExports();
|
||||||
const frontendCalls = getFrontendCalls();
|
const frontendCalls = getFrontendCalls();
|
||||||
const missingQuery = [];
|
const missingQuery: string[] = [];
|
||||||
|
|
||||||
frontendCalls.forEach((filename, funcName) => {
|
frontendCalls.forEach((filename, funcName) => {
|
||||||
if (!globalExports.has(funcName)) {
|
if (!globalExports.has(funcName)) {
|
||||||
|
|||||||
99
tools/validate_html.ts
Normal file
99
tools/validate_html.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const SRC_DIR = 'src';
|
||||||
|
|
||||||
|
async function validateHtmlFiles() {
|
||||||
|
console.log(`[HTML Validator] Scanning ${SRC_DIR} for HTML files...`);
|
||||||
|
|
||||||
|
// Find all HTML files
|
||||||
|
const htmlFiles = glob.sync(`${SRC_DIR}/**/*.html`);
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
for (const file of htmlFiles) {
|
||||||
|
const absolutPath = path.resolve(file);
|
||||||
|
const content = fs.readFileSync(absolutPath, 'utf-8');
|
||||||
|
|
||||||
|
// Load with source location info enabled
|
||||||
|
// Cast options to any to avoid TS version mismatches with cheerio types
|
||||||
|
const options: any = { sourceCodeLocationInfo: true };
|
||||||
|
const $ = cheerio.load(content, options);
|
||||||
|
|
||||||
|
const scripts = $('script').toArray();
|
||||||
|
|
||||||
|
for (const element of scripts) {
|
||||||
|
// Cast to any to access startIndex safely
|
||||||
|
const node = element as any;
|
||||||
|
|
||||||
|
// Skip external scripts
|
||||||
|
if ($(element).attr('src')) continue;
|
||||||
|
|
||||||
|
const scriptContent = $(element).html();
|
||||||
|
if (!scriptContent) continue;
|
||||||
|
|
||||||
|
// Determine start line of the script tag in the original file
|
||||||
|
// Cheerio (htmlparser2) location info:
|
||||||
|
const loc = node.startIndex !== undefined ?
|
||||||
|
getLineNumber(content, node.startIndex) : 1;
|
||||||
|
|
||||||
|
// Validate Syntax using TypeScript Compiler API
|
||||||
|
const sourceFile = ts.createSourceFile(
|
||||||
|
'virtual.js',
|
||||||
|
scriptContent,
|
||||||
|
ts.ScriptTarget.ES2020,
|
||||||
|
true, // setParentNodes
|
||||||
|
ts.ScriptKind.JS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cast to any because parseDiagnostics might not be in the public interface depending on version
|
||||||
|
const sf: any = sourceFile;
|
||||||
|
|
||||||
|
if (sf.parseDiagnostics && sf.parseDiagnostics.length > 0) {
|
||||||
|
hasErrors = true;
|
||||||
|
console.error(`\n❌ Syntax Error in ${file}`);
|
||||||
|
|
||||||
|
sf.parseDiagnostics.forEach((diag: any) => {
|
||||||
|
const { line, character } = sourceFile.getLineAndCharacterOfPosition(diag.start!);
|
||||||
|
const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
|
||||||
|
|
||||||
|
// Adjust line number: Script Start line + Error line inside script
|
||||||
|
// Note: 'line' is 0-indexed relative to script start
|
||||||
|
const visualLine = loc + line;
|
||||||
|
|
||||||
|
console.error(` Line ${visualLine}: ${message}`);
|
||||||
|
|
||||||
|
// Show snippet
|
||||||
|
const lines = scriptContent.split('\n');
|
||||||
|
if (lines[line]) {
|
||||||
|
console.error(` > ${lines[line].trim()}\n`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error(`\n[HTML Validator] Failed. Syntax errors detected.`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(`[HTML Validator] Passed. All HTML scripts are valid.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to calculate line number from char index
|
||||||
|
function getLineNumber(fullText: string, index: number): number {
|
||||||
|
return fullText.substring(0, index).split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if run directly
|
||||||
|
if (require.main === module) {
|
||||||
|
validateHtmlFiles().catch(err => {
|
||||||
|
console.error("Validator crashed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user