@things-factory/shell 9.2.5 → 10.0.0-beta.10
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/_index.html +1 -2
- package/client/{bootstrap.js → bootstrap.ts} +3 -2
- package/config/config.development.js +1 -1
- package/config/config.production.js +1 -1
- package/dist-server/graphql-local-client.js +1 -3
- package/dist-server/graphql-local-client.js.map +1 -1
- package/dist-server/server-dev.js +4 -3
- package/dist-server/server-dev.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/headless-pool/browser-factory.js +31 -1
- package/dist-server/utils/headless-pool/browser-factory.js.map +1 -1
- package/dist-server/utils/headless-pool/config.js +6 -2
- package/dist-server/utils/headless-pool/config.js.map +1 -1
- package/package.json +13 -20
- package/views/public/home.html +1 -1
- /package/client/{index.js → index.ts} +0 -0
- /package/client/{route.js → route.ts} +0 -0
- /package/client/serviceworker/{indexdb.js → indexdb.ts} +0 -0
- /package/client/serviceworker/{sw-src.js → sw-src.ts} +0 -0
- /package/client/{store.js → store.ts} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BrowserFactory = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
4
5
|
const env_1 = require("@things-factory/env");
|
|
5
6
|
// Dynamic puppeteer loading with error handling
|
|
6
7
|
let puppeteer;
|
|
@@ -10,6 +11,23 @@ try {
|
|
|
10
11
|
catch (err) {
|
|
11
12
|
env_1.logger.warn('Puppeteer not available:', err);
|
|
12
13
|
}
|
|
14
|
+
// Detect the best GL backend for the current environment (cached)
|
|
15
|
+
let detectedGLBackend = null;
|
|
16
|
+
function detectGLBackend() {
|
|
17
|
+
if (detectedGLBackend !== null)
|
|
18
|
+
return detectedGLBackend;
|
|
19
|
+
if (process.platform === 'darwin') {
|
|
20
|
+
detectedGLBackend = 'angle';
|
|
21
|
+
}
|
|
22
|
+
else if ((0, fs_1.existsSync)('/dev/nvidia0') || (0, fs_1.existsSync)('/dev/dri/renderD128')) {
|
|
23
|
+
detectedGLBackend = 'egl';
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
detectedGLBackend = 'swiftshader';
|
|
27
|
+
}
|
|
28
|
+
env_1.logger.info(`GL backend detected: ${detectedGLBackend}`);
|
|
29
|
+
return detectedGLBackend;
|
|
30
|
+
}
|
|
13
31
|
/**
|
|
14
32
|
* Browser Factory for creating and managing browser instances
|
|
15
33
|
*/
|
|
@@ -86,9 +104,21 @@ class BrowserFactory {
|
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
static getLaunchSettings(poolConfig) {
|
|
107
|
+
const glBackend = detectGLBackend();
|
|
108
|
+
// swiftshader 환경에서는 --use-gl= 플래그를 제거하고 --enable-unsafe-swiftshader만 사용
|
|
109
|
+
// Chrome 146+에서 --use-gl=swiftshader 및 --disable-gpu는 WebGL context 생성을 막음
|
|
110
|
+
const args = (poolConfig.args || []).flatMap(arg => {
|
|
111
|
+
if (arg.startsWith('--use-gl=')) {
|
|
112
|
+
return glBackend === 'swiftshader' ? [] : [`--use-gl=${glBackend}`];
|
|
113
|
+
}
|
|
114
|
+
return [arg];
|
|
115
|
+
});
|
|
116
|
+
if (glBackend === 'swiftshader') {
|
|
117
|
+
args.push('--enable-unsafe-swiftshader', '--use-angle=swiftshader');
|
|
118
|
+
}
|
|
89
119
|
const settings = {
|
|
90
120
|
headless: poolConfig.headless || 'shell',
|
|
91
|
-
args
|
|
121
|
+
args,
|
|
92
122
|
handleSIGINT: false, // ★ 기본 핸들러 해제
|
|
93
123
|
handleSIGTERM: false,
|
|
94
124
|
handleSIGHUP: false
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-factory.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/browser-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { config, logger } from '@things-factory/env'\nimport { HeadlessPoolConfig } from './config'\n\n// Dynamic puppeteer loading with error handling\nlet puppeteer: any\ntry {\n puppeteer = require('puppeteer')\n} catch (err) {\n logger.warn('Puppeteer not available:', err)\n}\n\n/**\n * Browser Factory for creating and managing browser instances\n */\nexport class BrowserFactory {\n /**\n * Create a new browser instance\n */\n static async createBrowser(poolConfig: HeadlessPoolConfig): Promise<any> {\n if (!puppeteer) {\n throw new Error('Puppeteer is not available')\n }\n\n const launchSettings = this.getLaunchSettings(poolConfig)\n\n try {\n const browser = await puppeteer.launch(launchSettings)\n logger.info(`Browser instance created with headless: ${launchSettings.headless}`)\n return browser\n } catch (error) {\n logger.error('Failed to create browser instance:', error)\n throw error\n }\n }\n\n /**\n * Validate browser instance\n */\n static validateBrowser(browser: any): Promise<boolean> {\n return Promise.race([\n new Promise<boolean>(resolve => setTimeout(() => resolve(false), 1500)),\n browser\n .version()\n .then(() => true)\n .catch(() => false)\n ])\n }\n\n /**\n * Safely destroy browser instance with multiple cleanup strategies\n */\n static async destroyBrowser(browser: any): Promise<void> {\n const timeout = 5000 // 5 second timeout\n\n try {\n await Promise.race([\n this.gracefulDestroy(browser),\n new Promise((_, reject) => setTimeout(() => reject(new Error('Browser destruction timeout')), timeout))\n ])\n\n logger.info('🗑️ Browser instance destroyed successfully')\n } catch (error) {\n logger.warn('⚠️ Error destroying browser instance:', error)\n\n // Force kill as last resort\n await this.forceKillProcess(browser)\n }\n }\n\n private static async gracefulDestroy(browser: any): Promise<void> {\n // Step 1: Close all pages\n await this.closeAllPages(browser)\n\n // Step 2: Standard close\n await browser.close()\n\n // Step 3: Kill browser process if still running\n await this.killBrowserProcess(browser)\n }\n\n /**\n * Create browser with custom setup (for special cases like label pool)\n */\n static async createBrowserWithSetup(\n poolConfig: HeadlessPoolConfig,\n customSetup: (browser: any) => Promise<any>\n ): Promise<any> {\n const browser = await this.createBrowser(poolConfig)\n\n try {\n const result = await customSetup(browser)\n return result\n } catch (error) {\n // If setup fails, cleanup the browser\n await this.destroyBrowser(browser)\n throw error\n }\n }\n\n private static getLaunchSettings(poolConfig: HeadlessPoolConfig) {\n const settings: any = {\n headless: poolConfig.headless || 'shell',\n args: [...(poolConfig.args || [])],\n handleSIGINT: false, // ★ 기본 핸들러 해제\n handleSIGTERM: false,\n handleSIGHUP: false\n }\n\n // Add executable path if specified in config or environment\n const executablePath = poolConfig.executablePath || config.get('CHROMIUM_PATH')\n if (executablePath) {\n settings.executablePath = executablePath\n }\n\n return settings\n }\n\n private static async closeAllPages(browser: any): Promise<void> {\n try {\n const pages = await browser.pages()\n await Promise.all(pages.map((page: any) => page.close().catch(() => {})))\n } catch (error) {\n logger.warn('Failed to close pages:', error)\n }\n }\n\n private static async killBrowserProcess(browser: any): Promise<void> {\n try {\n const process = browser.process()\n if (process && !process.killed) {\n // Try graceful termination first\n process.kill('SIGTERM')\n\n // Wait a bit, then force kill if still alive\n await new Promise(resolve => setTimeout(resolve, 1000))\n\n if (!process.killed) {\n process.kill('SIGKILL')\n logger.info('🔪 Browser process force killed')\n }\n }\n } catch (error) {\n logger.warn('Failed to kill browser process:', error)\n }\n }\n\n private static async forceKillProcess(browser: any): Promise<void> {\n try {\n const process = browser.process()\n if (process && !process.killed) {\n process.kill('SIGKILL')\n logger.info('💀 Browser process force killed')\n }\n } catch (killError) {\n logger.error('💀 Failed to force kill browser process:', killError)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"browser-factory.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/browser-factory.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'fs'\nimport { config, logger } from '@things-factory/env'\nimport { HeadlessPoolConfig } from './config'\n\n// Dynamic puppeteer loading with error handling\nlet puppeteer: any\ntry {\n puppeteer = require('puppeteer')\n} catch (err) {\n logger.warn('Puppeteer not available:', err)\n}\n\n// Detect the best GL backend for the current environment (cached)\nlet detectedGLBackend: string | null = null\n\nfunction detectGLBackend(): string {\n if (detectedGLBackend !== null) return detectedGLBackend\n\n if (process.platform === 'darwin') {\n detectedGLBackend = 'angle'\n } else if (existsSync('/dev/nvidia0') || existsSync('/dev/dri/renderD128')) {\n detectedGLBackend = 'egl'\n } else {\n detectedGLBackend = 'swiftshader'\n }\n\n logger.info(`GL backend detected: ${detectedGLBackend}`)\n return detectedGLBackend\n}\n\n/**\n * Browser Factory for creating and managing browser instances\n */\nexport class BrowserFactory {\n /**\n * Create a new browser instance\n */\n static async createBrowser(poolConfig: HeadlessPoolConfig): Promise<any> {\n if (!puppeteer) {\n throw new Error('Puppeteer is not available')\n }\n\n const launchSettings = this.getLaunchSettings(poolConfig)\n\n try {\n const browser = await puppeteer.launch(launchSettings)\n logger.info(`Browser instance created with headless: ${launchSettings.headless}`)\n return browser\n } catch (error) {\n logger.error('Failed to create browser instance:', error)\n throw error\n }\n }\n\n /**\n * Validate browser instance\n */\n static validateBrowser(browser: any): Promise<boolean> {\n return Promise.race([\n new Promise<boolean>(resolve => setTimeout(() => resolve(false), 1500)),\n browser\n .version()\n .then(() => true)\n .catch(() => false)\n ])\n }\n\n /**\n * Safely destroy browser instance with multiple cleanup strategies\n */\n static async destroyBrowser(browser: any): Promise<void> {\n const timeout = 5000 // 5 second timeout\n\n try {\n await Promise.race([\n this.gracefulDestroy(browser),\n new Promise((_, reject) => setTimeout(() => reject(new Error('Browser destruction timeout')), timeout))\n ])\n\n logger.info('🗑️ Browser instance destroyed successfully')\n } catch (error) {\n logger.warn('⚠️ Error destroying browser instance:', error)\n\n // Force kill as last resort\n await this.forceKillProcess(browser)\n }\n }\n\n private static async gracefulDestroy(browser: any): Promise<void> {\n // Step 1: Close all pages\n await this.closeAllPages(browser)\n\n // Step 2: Standard close\n await browser.close()\n\n // Step 3: Kill browser process if still running\n await this.killBrowserProcess(browser)\n }\n\n /**\n * Create browser with custom setup (for special cases like label pool)\n */\n static async createBrowserWithSetup(\n poolConfig: HeadlessPoolConfig,\n customSetup: (browser: any) => Promise<any>\n ): Promise<any> {\n const browser = await this.createBrowser(poolConfig)\n\n try {\n const result = await customSetup(browser)\n return result\n } catch (error) {\n // If setup fails, cleanup the browser\n await this.destroyBrowser(browser)\n throw error\n }\n }\n\n private static getLaunchSettings(poolConfig: HeadlessPoolConfig) {\n const glBackend = detectGLBackend()\n\n // swiftshader 환경에서는 --use-gl= 플래그를 제거하고 --enable-unsafe-swiftshader만 사용\n // Chrome 146+에서 --use-gl=swiftshader 및 --disable-gpu는 WebGL context 생성을 막음\n const args = (poolConfig.args || []).flatMap(arg => {\n if (arg.startsWith('--use-gl=')) {\n return glBackend === 'swiftshader' ? [] : [`--use-gl=${glBackend}`]\n }\n return [arg]\n })\n\n if (glBackend === 'swiftshader') {\n args.push(\n '--enable-unsafe-swiftshader',\n '--use-angle=swiftshader'\n )\n }\n\n const settings: any = {\n headless: poolConfig.headless || 'shell',\n args,\n handleSIGINT: false, // ★ 기본 핸들러 해제\n handleSIGTERM: false,\n handleSIGHUP: false\n }\n\n // Add executable path if specified in config or environment\n const executablePath = poolConfig.executablePath || config.get('CHROMIUM_PATH')\n if (executablePath) {\n settings.executablePath = executablePath\n }\n\n return settings\n }\n\n private static async closeAllPages(browser: any): Promise<void> {\n try {\n const pages = await browser.pages()\n await Promise.all(pages.map((page: any) => page.close().catch(() => {})))\n } catch (error) {\n logger.warn('Failed to close pages:', error)\n }\n }\n\n private static async killBrowserProcess(browser: any): Promise<void> {\n try {\n const process = browser.process()\n if (process && !process.killed) {\n // Try graceful termination first\n process.kill('SIGTERM')\n\n // Wait a bit, then force kill if still alive\n await new Promise(resolve => setTimeout(resolve, 1000))\n\n if (!process.killed) {\n process.kill('SIGKILL')\n logger.info('🔪 Browser process force killed')\n }\n }\n } catch (error) {\n logger.warn('Failed to kill browser process:', error)\n }\n }\n\n private static async forceKillProcess(browser: any): Promise<void> {\n try {\n const process = browser.process()\n if (process && !process.killed) {\n process.kill('SIGKILL')\n logger.info('💀 Browser process force killed')\n }\n } catch (killError) {\n logger.error('💀 Failed to force kill browser process:', killError)\n }\n }\n}\n"]}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HEADLESS_POOL_ARGUMENT_SETS = exports.DEFAULT_POOL_CONFIG = void 0;
|
|
7
7
|
exports.mergeConfig = mergeConfig;
|
|
8
|
+
// macOS에서는 EGL 사용 불가 → angle 사용
|
|
9
|
+
const glBackend = process.platform === 'darwin' ? 'angle' : 'egl';
|
|
8
10
|
// Default configurations
|
|
9
11
|
exports.DEFAULT_POOL_CONFIG = {
|
|
10
12
|
// Pool defaults
|
|
@@ -19,7 +21,9 @@ exports.DEFAULT_POOL_CONFIG = {
|
|
|
19
21
|
'--hide-scrollbars',
|
|
20
22
|
'--mute-audio',
|
|
21
23
|
'--no-sandbox',
|
|
22
|
-
|
|
24
|
+
`--use-gl=${glBackend}`,
|
|
25
|
+
'--enable-webgl',
|
|
26
|
+
'--ignore-gpu-blocklist',
|
|
23
27
|
'--use-mock-keychain',
|
|
24
28
|
'--disable-password-manager-reauthentication',
|
|
25
29
|
'--disable-keychain-reauthorization',
|
|
@@ -42,7 +46,7 @@ exports.DEFAULT_POOL_CONFIG = {
|
|
|
42
46
|
};
|
|
43
47
|
// Common argument sets
|
|
44
48
|
exports.HEADLESS_POOL_ARGUMENT_SETS = {
|
|
45
|
-
basic: ['--hide-scrollbars', '--mute-audio', '--no-sandbox',
|
|
49
|
+
basic: ['--hide-scrollbars', '--mute-audio', '--no-sandbox', `--use-gl=${glBackend}`, '--enable-webgl', '--ignore-gpu-blocklist'],
|
|
46
50
|
keychain_safe: [
|
|
47
51
|
'--use-mock-keychain',
|
|
48
52
|
'--disable-password-manager-reauthentication',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/config.ts"],"names":[],"mappings":";AAAA;;GAEG;;;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../server/utils/headless-pool/config.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AA6FH,kCAOC;AA1ED,gCAAgC;AAChC,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;AAEjE,yBAAyB;AACZ,QAAA,mBAAmB,GAAsD;IACpF,gBAAgB;IAChB,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,EAAE;IACP,oBAAoB,EAAE,KAAK;IAC3B,iBAAiB,EAAE,MAAM,EAAE,YAAY;IACvC,YAAY,EAAE,IAAI;IAElB,mBAAmB;IACnB,QAAQ,EAAE,OAAO;IACjB,IAAI,EAAE;QACJ,mBAAmB;QACnB,cAAc;QACd,cAAc;QACd,YAAY,SAAS,EAAE;QACvB,gBAAgB;QAChB,wBAAwB;QACxB,qBAAqB;QACrB,6CAA6C;QAC7C,oCAAoC;QACpC,wBAAwB;QACxB,iCAAiC;QACjC,sBAAsB;QACtB,wBAAwB;QACxB,sDAAsD;QACtD,uCAAuC;QACvC,iCAAiC;QACjC,gBAAgB;QAChB,gCAAgC;QAChC,mCAAmC;KACpC;IACD,cAAc,EAAE,EAAE;IAElB,mBAAmB;IACnB,WAAW,EAAE,KAAK;IAClB,cAAc,EAAE,KAAK;IACrB,aAAa,EAAE,IAAI;CACpB,CAAA;AAED,uBAAuB;AACV,QAAA,2BAA2B,GAAG;IACzC,KAAK,EAAE,CAAC,mBAAmB,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,SAAS,EAAE,EAAE,gBAAgB,EAAE,wBAAwB,CAAC;IAEjI,aAAa,EAAE;QACb,qBAAqB;QACrB,6CAA6C;QAC7C,oCAAoC;QACpC,wBAAwB;QACxB,iCAAiC;QACjC,sBAAsB;QACtB,wBAAwB;QACxB,sDAAsD;QACtD,uCAAuC;QACvC,iCAAiC;QACjC,gBAAgB;QAChB,gCAAgC;QAChC,mCAAmC;KACpC;IAED,iBAAiB,EAAE,CAAC,0BAA0B,EAAE,yBAAyB,CAAC;CAC3E,CAAA;AAED,mCAAmC;AACnC,SAAgB,WAAW,CAAC,IAAwB,EAAE,WAA+B,EAAE;IACrF,OAAO;QACL,GAAG,2BAAmB;QACtB,GAAG,IAAI;QACP,GAAG,QAAQ;QACX,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,2BAAmB,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;KAC7E,CAAA;AACH,CAAC","sourcesContent":["/**\n * Headless Pool Configuration\n */\n\nexport type HeadlessMode = 'shell' | 'new' | boolean\n\nexport interface HeadlessPoolConfig {\n // Pool settings\n min?: number\n max?: number\n acquireTimeoutMillis?: number\n idleTimeoutMillis?: number\n testOnBorrow?: boolean\n\n // Browser launch settings\n headless?: HeadlessMode\n args?: string[]\n executablePath?: string\n\n // Features\n enableStats?: boolean\n enableRecovery?: boolean\n enableCleanup?: boolean\n\n // Custom setup function for special cases (like label pool)\n customSetup?: (browser: any) => Promise<any>\n}\n\n// macOS에서는 EGL 사용 불가 → angle 사용\nconst glBackend = process.platform === 'darwin' ? 'angle' : 'egl'\n\n// Default configurations\nexport const DEFAULT_POOL_CONFIG: Required<Omit<HeadlessPoolConfig, 'customSetup'>> = {\n // Pool defaults\n min: 2,\n max: 10,\n acquireTimeoutMillis: 15000,\n idleTimeoutMillis: 300000, // 5 minutes\n testOnBorrow: true,\n\n // Browser defaults\n headless: 'shell',\n args: [\n '--hide-scrollbars',\n '--mute-audio',\n '--no-sandbox',\n `--use-gl=${glBackend}`,\n '--enable-webgl',\n '--ignore-gpu-blocklist',\n '--use-mock-keychain',\n '--disable-password-manager-reauthentication',\n '--disable-keychain-reauthorization',\n '--disable-web-security',\n '--disable-site-isolation-trials',\n '--disable-extensions',\n '--disable-default-apps',\n '--disable-component-extensions-with-background-pages',\n '--disable-background-timer-throttling',\n '--disable-background-networking',\n '--disable-sync',\n '--disable-features=TranslateUI',\n '--disable-ipc-flooding-protection'\n ],\n executablePath: '',\n\n // Feature defaults\n enableStats: false,\n enableRecovery: false,\n enableCleanup: true\n}\n\n// Common argument sets\nexport const HEADLESS_POOL_ARGUMENT_SETS = {\n basic: ['--hide-scrollbars', '--mute-audio', '--no-sandbox', `--use-gl=${glBackend}`, '--enable-webgl', '--ignore-gpu-blocklist'],\n\n keychain_safe: [\n '--use-mock-keychain',\n '--disable-password-manager-reauthentication',\n '--disable-keychain-reauthorization',\n '--disable-web-security',\n '--disable-site-isolation-trials',\n '--disable-extensions',\n '--disable-default-apps',\n '--disable-component-extensions-with-background-pages',\n '--disable-background-timer-throttling',\n '--disable-background-networking',\n '--disable-sync',\n '--disable-features=TranslateUI',\n '--disable-ipc-flooding-protection'\n ],\n\n security_enhanced: ['--disable-setuid-sandbox', '--disable-dev-shm-usage']\n}\n\n// Helper function to merge configs\nexport function mergeConfig(base: HeadlessPoolConfig, override: HeadlessPoolConfig = {}): HeadlessPoolConfig {\n return {\n ...DEFAULT_POOL_CONFIG,\n ...base,\n ...override,\n args: [...(base.args || DEFAULT_POOL_CONFIG.args), ...(override.args || [])]\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0-beta.10",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"things-factory-dockerize-arm": "bin/things-factory-dockerize-arm"
|
|
12
12
|
},
|
|
13
13
|
"main": "dist-server/index.js",
|
|
14
|
-
"browser": "client/index.
|
|
14
|
+
"browser": "client/index.ts",
|
|
15
15
|
"author": "Hearty Oh <heartyoh@hatiolab.com>",
|
|
16
16
|
"things-factory": true,
|
|
17
17
|
"license": "MIT",
|
|
@@ -44,25 +44,20 @@
|
|
|
44
44
|
"@graphql-tools/utils": "^10.1.2",
|
|
45
45
|
"@graphql-yoga/redis-event-target": "^3.0.1",
|
|
46
46
|
"@koa/cors": "^5.0.0",
|
|
47
|
-
"@material/mwc-button": "^0.27.0",
|
|
48
|
-
"@material/mwc-icon": "^0.27.0",
|
|
49
|
-
"@material/mwc-icon-button": "^0.27.0",
|
|
50
|
-
"@material/mwc-slider": "^0.27.0",
|
|
51
|
-
"@material/mwc-textfield": "^0.27.0",
|
|
52
47
|
"@material/web": "^2.0.0",
|
|
53
48
|
"@open-wc/scoped-elements": "^2.1.3",
|
|
54
|
-
"@operato/graphql": "^
|
|
55
|
-
"@operato/help": "^
|
|
56
|
-
"@operato/layout": "^
|
|
57
|
-
"@operato/shell": "^
|
|
58
|
-
"@operato/typeorm-history": "^
|
|
59
|
-
"@operato/utils": "^
|
|
49
|
+
"@operato/graphql": "^10.0.0-beta.1",
|
|
50
|
+
"@operato/help": "^10.0.0-beta.1",
|
|
51
|
+
"@operato/layout": "^10.0.0-beta.1",
|
|
52
|
+
"@operato/shell": "^10.0.0-beta.1",
|
|
53
|
+
"@operato/typeorm-history": "^10.0.0-beta.1",
|
|
54
|
+
"@operato/utils": "^10.0.0-beta.1",
|
|
60
55
|
"@reduxjs/toolkit": "^2.2.5",
|
|
61
|
-
"@things-factory/ejs-remote": "^
|
|
62
|
-
"@things-factory/env": "^
|
|
56
|
+
"@things-factory/ejs-remote": "^10.0.0-beta.5",
|
|
57
|
+
"@things-factory/env": "^10.0.0-beta.7",
|
|
63
58
|
"@things-factory/operato-license-checker": "^4.0.4",
|
|
64
|
-
"@things-factory/styles": "^
|
|
65
|
-
"@things-factory/utils": "^
|
|
59
|
+
"@things-factory/styles": "^10.0.0-beta.5",
|
|
60
|
+
"@things-factory/utils": "^10.0.0-beta.5",
|
|
66
61
|
"@webcomponents/scoped-custom-element-registry": "^0.0.9",
|
|
67
62
|
"@webcomponents/webcomponentsjs": "^2.6.0",
|
|
68
63
|
"args": "^5.0.0",
|
|
@@ -107,7 +102,6 @@
|
|
|
107
102
|
"pluralize": "^8.0.0",
|
|
108
103
|
"promises-all": "^1.0.0",
|
|
109
104
|
"puppeteer": "^24.5.0",
|
|
110
|
-
"pwa-helpers": "^0.9.1",
|
|
111
105
|
"react": "^18.2.0",
|
|
112
106
|
"react-dom": "^18.2.0",
|
|
113
107
|
"reflect-metadata": "^0.2.2",
|
|
@@ -118,7 +112,6 @@
|
|
|
118
112
|
"type-graphql": "^2.0.0-rc.2",
|
|
119
113
|
"typeorm": "^0.3.19",
|
|
120
114
|
"uuid": "^10.0.0",
|
|
121
|
-
"web-animations-js": "^2.3.2",
|
|
122
115
|
"web-push": "^3.5.0",
|
|
123
116
|
"webpack-dev-middleware": "^7.4.2",
|
|
124
117
|
"ws": "^8.8.1"
|
|
@@ -131,5 +124,5 @@
|
|
|
131
124
|
"pg": "^8.7.3",
|
|
132
125
|
"sqlite3": "^5.0.8"
|
|
133
126
|
},
|
|
134
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "95acadd39e9a0ff3b2f34d9f7082142395903179"
|
|
135
128
|
}
|
package/views/public/home.html
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<noscript> Please enable JavaScript to view this website. </noscript>
|
|
67
67
|
<!-- Load webcomponents-loader.js to check and load any polyfills your browser needs -->
|
|
68
68
|
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
<!-- Built with love using PWA Starter Kit -->
|
|
71
71
|
|
|
72
72
|
<script src="/public/home.js"></script>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|