@screenly/edge-apps 0.1.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -21
- package/bin/edge-apps-scripts.js +5 -0
- package/configs/playwright.js +1 -1
- package/eslint.config.ts +7 -0
- package/package.json +20 -12
- package/scripts/{cli.ts → cli.js} +23 -40
- package/scripts/{copy-assets.ts → copy-assets.js} +1 -7
- package/scripts/cors-proxy-server.js +190 -0
- package/scripts/{create.ts → create.js} +10 -13
- package/tsconfig.json +1 -1
- package/bin/edge-apps-scripts.ts +0 -9
- package/scripts/cors-proxy-server.ts +0 -154
package/README.md
CHANGED
|
@@ -6,8 +6,14 @@ A TypeScript library for interfacing with the Screenly Edge Apps API.
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
> [!
|
|
10
|
-
>
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> Requires Node.js >= 20.6.0.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @screenly/edge-apps
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or with Bun:
|
|
11
17
|
|
|
12
18
|
```bash
|
|
13
19
|
bun add @screenly/edge-apps
|
|
@@ -20,26 +26,22 @@ When developing Edge Apps locally using the library from this repository, you sh
|
|
|
20
26
|
First, in the root of this repository:
|
|
21
27
|
|
|
22
28
|
```bash
|
|
23
|
-
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
npm link
|
|
24
32
|
```
|
|
25
33
|
|
|
26
34
|
Then in your Edge App directory:
|
|
27
35
|
|
|
28
36
|
```bash
|
|
29
37
|
cd /path/to/your-edge-app
|
|
30
|
-
|
|
38
|
+
npm link @screenly/edge-apps
|
|
31
39
|
```
|
|
32
40
|
|
|
33
41
|
To unlink the package when you're done with local development, run the following in your Edge App directory:
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Then in the root of this repository:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
bun unlink
|
|
44
|
+
npm install
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
## Quick Start
|
|
@@ -97,7 +99,7 @@ signalReady()
|
|
|
97
99
|
|
|
98
100
|
## Web Components
|
|
99
101
|
|
|
100
|
-
This library includes reusable web components for building consistent Edge Apps. See the [components documentation](https://github.com/Screenly/edge-apps-library/blob/
|
|
102
|
+
This library includes reusable web components for building consistent Edge Apps. See the [components documentation](https://github.com/Screenly/edge-apps-library/blob/main/docs/components.md) for usage details.
|
|
101
103
|
|
|
102
104
|
## Edge Apps Scripts CLI
|
|
103
105
|
|
|
@@ -110,13 +112,13 @@ This package provides the `edge-apps-scripts` CLI tool for running shared develo
|
|
|
110
112
|
Start the Vite development server with mock data from `screenly.yml` and `mock-data.yml`:
|
|
111
113
|
|
|
112
114
|
```bash
|
|
113
|
-
|
|
115
|
+
npm run dev
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
#### Building
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
|
-
|
|
121
|
+
npm run build
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
#### Linting
|
|
@@ -124,13 +126,13 @@ bun run build
|
|
|
124
126
|
To lint your Edge App:
|
|
125
127
|
|
|
126
128
|
```bash
|
|
127
|
-
|
|
129
|
+
npm run lint
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
To lint and automatically fix issues:
|
|
131
133
|
|
|
132
134
|
```bash
|
|
133
|
-
|
|
135
|
+
npm run lint -- --fix
|
|
134
136
|
```
|
|
135
137
|
|
|
136
138
|
#### Type Checking
|
|
@@ -138,7 +140,7 @@ bun run lint -- --fix
|
|
|
138
140
|
Run TypeScript type checking:
|
|
139
141
|
|
|
140
142
|
```bash
|
|
141
|
-
|
|
143
|
+
npm run type-check
|
|
142
144
|
```
|
|
143
145
|
|
|
144
146
|
### Command-Line Utilities for Edge Apps
|
|
@@ -158,7 +160,7 @@ It is recommended to add the following scripts to your Edge App's `package.json`
|
|
|
158
160
|
"build:dev": "edge-apps-scripts build:dev",
|
|
159
161
|
"lint": "edge-apps-scripts lint",
|
|
160
162
|
"type-check": "edge-apps-scripts type-check",
|
|
161
|
-
"deploy": "
|
|
163
|
+
"deploy": "npm run build && screenly edge-app deploy --path=dist/"
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
```
|
|
@@ -197,7 +199,7 @@ import type {
|
|
|
197
199
|
## Development
|
|
198
200
|
|
|
199
201
|
```bash
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
npm install # Install dependencies
|
|
203
|
+
npm test # Run tests
|
|
204
|
+
npm run build # Build library
|
|
203
205
|
```
|
package/configs/playwright.js
CHANGED
|
@@ -18,7 +18,7 @@ try {
|
|
|
18
18
|
} catch {
|
|
19
19
|
throw new Error(
|
|
20
20
|
"Failed to resolve '@playwright/test' from the application. " +
|
|
21
|
-
"Please install it in your app (e.g. '
|
|
21
|
+
"Please install it in your app (e.g. 'npm install -D @playwright/test') " +
|
|
22
22
|
'and try again.',
|
|
23
23
|
)
|
|
24
24
|
}
|
package/eslint.config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import eslint from '@eslint/js'
|
|
2
2
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
3
|
+
import globals from 'globals'
|
|
3
4
|
import tseslint from 'typescript-eslint'
|
|
4
5
|
|
|
5
6
|
export default defineConfig(
|
|
@@ -34,6 +35,12 @@ export default defineConfig(
|
|
|
34
35
|
}],
|
|
35
36
|
},
|
|
36
37
|
},
|
|
38
|
+
{
|
|
39
|
+
files: ['scripts/**/*.js', 'bin/**/*.js'],
|
|
40
|
+
languageOptions: {
|
|
41
|
+
globals: globals.node,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
37
44
|
{
|
|
38
45
|
files: ['**/*.test.ts', '**/*.spec.ts'],
|
|
39
46
|
rules: {
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@screenly/edge-apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"description": "A TypeScript library for interfacing with Screenly Edge Apps API",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.6.0"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.js",
|
|
7
10
|
"module": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
9
12
|
"bin": {
|
|
10
|
-
"edge-apps-scripts": "./bin/edge-apps-scripts.
|
|
13
|
+
"edge-apps-scripts": "./bin/edge-apps-scripts.js"
|
|
11
14
|
},
|
|
12
15
|
"exports": {
|
|
13
16
|
".": {
|
|
@@ -55,14 +58,14 @@
|
|
|
55
58
|
"src/assets"
|
|
56
59
|
],
|
|
57
60
|
"scripts": {
|
|
58
|
-
"prepublishOnly": "
|
|
59
|
-
"prebuild": "
|
|
60
|
-
"build": "
|
|
61
|
+
"prepublishOnly": "npm run build",
|
|
62
|
+
"prebuild": "npm run type-check",
|
|
63
|
+
"build": "npm run build:types && npm run copy:assets",
|
|
61
64
|
"build:types": "tsc",
|
|
62
|
-
"copy:assets": "
|
|
63
|
-
"test": "
|
|
64
|
-
"test:unit": "
|
|
65
|
-
"test:coverage": "
|
|
65
|
+
"copy:assets": "node scripts/copy-assets.js",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:unit": "vitest run",
|
|
68
|
+
"test:coverage": "vitest run --coverage",
|
|
66
69
|
"lint": "eslint . --fix",
|
|
67
70
|
"format": "prettier --write src/ scripts/ vite-plugins/ README.md index.html",
|
|
68
71
|
"format:check": "prettier --check src/ scripts/ vite-plugins/ README.md index.html",
|
|
@@ -76,6 +79,10 @@
|
|
|
76
79
|
],
|
|
77
80
|
"author": "Screenly",
|
|
78
81
|
"license": "MIT",
|
|
82
|
+
"repository": {
|
|
83
|
+
"type": "git",
|
|
84
|
+
"url": "https://github.com/Screenly/edge-apps-library"
|
|
85
|
+
},
|
|
79
86
|
"publishConfig": {
|
|
80
87
|
"access": "public"
|
|
81
88
|
},
|
|
@@ -105,12 +112,13 @@
|
|
|
105
112
|
}
|
|
106
113
|
},
|
|
107
114
|
"devDependencies": {
|
|
108
|
-
"@types/bun": "^1.3.10",
|
|
109
115
|
"@types/jsdom": "^28.0.0",
|
|
110
116
|
"@types/node": "^25.4.0",
|
|
111
117
|
"@types/sharp": "^0.32.0",
|
|
112
|
-
"
|
|
118
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
119
|
+
"globals": "^13.24.0",
|
|
113
120
|
"jsdom": "^28.1.0",
|
|
114
|
-
"prettier": "^3.8.1"
|
|
121
|
+
"prettier": "^3.8.1",
|
|
122
|
+
"vitest": "^3.2.4"
|
|
115
123
|
}
|
|
116
124
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
/**
|
|
3
|
-
* CLI command dispatcher for edge-apps-scripts
|
|
4
|
-
*/
|
|
1
|
+
#!/usr/bin/env node
|
|
5
2
|
|
|
6
|
-
import { execSync, execFileSync, spawn
|
|
3
|
+
import { execSync, execFileSync, spawn } from 'child_process'
|
|
7
4
|
import fs from 'fs'
|
|
8
5
|
import path from 'path'
|
|
9
6
|
import { fileURLToPath } from 'url'
|
|
10
|
-
import { createCommand } from './create'
|
|
7
|
+
import { createCommand } from './create.js'
|
|
8
|
+
import { start as startCorsProxy } from './cors-proxy-server.js'
|
|
11
9
|
|
|
12
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
11
|
const __dirname = path.dirname(__filename)
|
|
@@ -38,6 +36,10 @@ const commands = {
|
|
|
38
36
|
description: 'Capture screenshots at all supported resolutions',
|
|
39
37
|
handler: screenshotsCommand,
|
|
40
38
|
},
|
|
39
|
+
'cors-proxy': {
|
|
40
|
+
description: 'Start the CORS proxy server for local development',
|
|
41
|
+
handler: startCorsProxy,
|
|
42
|
+
},
|
|
41
43
|
create: {
|
|
42
44
|
description:
|
|
43
45
|
'Initialize a scaffolded Edge App (replaces template placeholders)',
|
|
@@ -45,20 +47,14 @@ const commands = {
|
|
|
45
47
|
},
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
* Helper: Resolve a binary from the app's node_modules first, falling back to the library's
|
|
50
|
-
*/
|
|
51
|
-
function resolveBin(name: string): string {
|
|
50
|
+
function resolveBin(name) {
|
|
52
51
|
const appBin = path.resolve(process.cwd(), 'node_modules', '.bin', name)
|
|
53
52
|
return fs.existsSync(appBin)
|
|
54
53
|
? appBin
|
|
55
54
|
: path.resolve(libraryRoot, 'node_modules', '.bin', name)
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
* Helper: Get NODE_PATH with library's node_modules included
|
|
60
|
-
*/
|
|
61
|
-
function getNodePath(): string {
|
|
57
|
+
function getNodePath() {
|
|
62
58
|
const libraryNodeModules = path.resolve(libraryRoot, 'node_modules')
|
|
63
59
|
const existingNodePath = process.env.NODE_PATH || ''
|
|
64
60
|
return existingNodePath
|
|
@@ -66,26 +62,16 @@ function getNodePath(): string {
|
|
|
66
62
|
: libraryNodeModules
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
function setupSignalHandlers(child: ChildProcess): void {
|
|
73
|
-
const handleSignal = (signal: string) => {
|
|
74
|
-
child.kill(signal as NodeJS.Signals)
|
|
65
|
+
function setupSignalHandlers(child) {
|
|
66
|
+
const handleSignal = (signal) => {
|
|
67
|
+
child.kill(signal)
|
|
75
68
|
child.on('exit', () => process.exit(0))
|
|
76
69
|
}
|
|
77
70
|
process.on('SIGINT', () => handleSignal('SIGINT'))
|
|
78
71
|
process.on('SIGTERM', () => handleSignal('SIGTERM'))
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
* Helper: Spawn a process with common options and signal handling
|
|
83
|
-
*/
|
|
84
|
-
function spawnWithSignalHandling(
|
|
85
|
-
command: string,
|
|
86
|
-
args: string[],
|
|
87
|
-
errorMessage: string,
|
|
88
|
-
): void {
|
|
74
|
+
function spawnWithSignalHandling(command, args, errorMessage) {
|
|
89
75
|
const child = spawn(command, args, {
|
|
90
76
|
stdio: 'inherit',
|
|
91
77
|
cwd: process.cwd(),
|
|
@@ -104,17 +90,14 @@ function spawnWithSignalHandling(
|
|
|
104
90
|
setupSignalHandlers(child)
|
|
105
91
|
}
|
|
106
92
|
|
|
107
|
-
|
|
108
|
-
* Helper: Get Vite binary and config paths
|
|
109
|
-
*/
|
|
110
|
-
function getVitePaths(): { viteBin: string; configPath: string } {
|
|
93
|
+
function getVitePaths() {
|
|
111
94
|
return {
|
|
112
95
|
viteBin: resolveBin('vite'),
|
|
113
96
|
configPath: path.resolve(libraryRoot, 'vite.config.ts'),
|
|
114
97
|
}
|
|
115
98
|
}
|
|
116
99
|
|
|
117
|
-
async function lintCommand(args
|
|
100
|
+
async function lintCommand(args) {
|
|
118
101
|
try {
|
|
119
102
|
const eslintBin = resolveBin('eslint')
|
|
120
103
|
|
|
@@ -137,7 +120,7 @@ async function lintCommand(args: string[]) {
|
|
|
137
120
|
}
|
|
138
121
|
}
|
|
139
122
|
|
|
140
|
-
async function devCommand(args
|
|
123
|
+
async function devCommand(args) {
|
|
141
124
|
try {
|
|
142
125
|
const { viteBin, configPath } = getVitePaths()
|
|
143
126
|
const viteArgs = ['--config', configPath, ...args]
|
|
@@ -148,7 +131,7 @@ async function devCommand(args: string[]) {
|
|
|
148
131
|
}
|
|
149
132
|
}
|
|
150
133
|
|
|
151
|
-
async function buildCommand(args
|
|
134
|
+
async function buildCommand(args) {
|
|
152
135
|
try {
|
|
153
136
|
const { viteBin, configPath } = getVitePaths()
|
|
154
137
|
const sourcemaps = args.includes('--sourcemaps')
|
|
@@ -174,7 +157,7 @@ async function buildCommand(args: string[]) {
|
|
|
174
157
|
}
|
|
175
158
|
}
|
|
176
159
|
|
|
177
|
-
async function buildDevCommand(args
|
|
160
|
+
async function buildDevCommand(args) {
|
|
178
161
|
try {
|
|
179
162
|
const { viteBin, configPath } = getVitePaths()
|
|
180
163
|
const viteArgs = [
|
|
@@ -192,7 +175,7 @@ async function buildDevCommand(args: string[]) {
|
|
|
192
175
|
}
|
|
193
176
|
}
|
|
194
177
|
|
|
195
|
-
async function typeCheckCommand(args
|
|
178
|
+
async function typeCheckCommand(args) {
|
|
196
179
|
try {
|
|
197
180
|
const tscBin = resolveBin('tsc')
|
|
198
181
|
|
|
@@ -212,7 +195,7 @@ async function typeCheckCommand(args: string[]) {
|
|
|
212
195
|
}
|
|
213
196
|
}
|
|
214
197
|
|
|
215
|
-
async function convertPngsToWebP(screenshotsDir
|
|
198
|
+
async function convertPngsToWebP(screenshotsDir) {
|
|
216
199
|
const { default: sharp } = await import('sharp')
|
|
217
200
|
const pngFiles = fs
|
|
218
201
|
.readdirSync(screenshotsDir)
|
|
@@ -226,7 +209,7 @@ async function convertPngsToWebP(screenshotsDir: string): Promise<void> {
|
|
|
226
209
|
}
|
|
227
210
|
}
|
|
228
211
|
|
|
229
|
-
async function screenshotsCommand(_args
|
|
212
|
+
async function screenshotsCommand(_args) {
|
|
230
213
|
try {
|
|
231
214
|
const playwrightBin = resolveBin('playwright')
|
|
232
215
|
const playwrightConfig = path.resolve(
|
|
@@ -280,7 +263,7 @@ export async function run() {
|
|
|
280
263
|
process.exit(1)
|
|
281
264
|
}
|
|
282
265
|
|
|
283
|
-
const handler = commands[cmd
|
|
266
|
+
const handler = commands[cmd].handler
|
|
284
267
|
await handler(cmdArgs)
|
|
285
268
|
}
|
|
286
269
|
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
/**
|
|
3
|
-
* Copy assets from src to dist after TypeScript compilation
|
|
4
|
-
*/
|
|
1
|
+
#!/usr/bin/env node
|
|
5
2
|
|
|
6
3
|
import { cpSync, existsSync, mkdirSync } from 'fs'
|
|
7
4
|
import { dirname } from 'path'
|
|
@@ -15,10 +12,7 @@ const srcAssets = `${libraryRoot}/src/assets`
|
|
|
15
12
|
const distAssets = `${libraryRoot}/dist/assets`
|
|
16
13
|
|
|
17
14
|
if (existsSync(srcAssets)) {
|
|
18
|
-
// Ensure dist directory exists
|
|
19
15
|
mkdirSync(dirname(distAssets), { recursive: true })
|
|
20
|
-
|
|
21
|
-
// Copy assets
|
|
22
16
|
cpSync(srcAssets, distAssets, { recursive: true })
|
|
23
17
|
console.log('✓ Copied assets to dist/')
|
|
24
18
|
} else {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import https from 'https'
|
|
3
|
+
import { URL } from 'url'
|
|
4
|
+
|
|
5
|
+
const MAX_REDIRECTS = 10
|
|
6
|
+
|
|
7
|
+
function setCorsHeaders(res) {
|
|
8
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
9
|
+
res.setHeader(
|
|
10
|
+
'Access-Control-Allow-Methods',
|
|
11
|
+
'GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH',
|
|
12
|
+
)
|
|
13
|
+
res.setHeader('Access-Control-Allow-Headers', '*')
|
|
14
|
+
res.setHeader('Access-Control-Max-Age', '86400')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function handleRedirect(
|
|
18
|
+
location,
|
|
19
|
+
req,
|
|
20
|
+
res,
|
|
21
|
+
proxyRes,
|
|
22
|
+
currentUrl,
|
|
23
|
+
redirectCount,
|
|
24
|
+
) {
|
|
25
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
26
|
+
proxyRes.resume()
|
|
27
|
+
res.writeHead(508, { 'Content-Type': 'application/json' })
|
|
28
|
+
res.end(JSON.stringify({ error: 'Too many redirects' }))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const redirectUrl = new URL(location, currentUrl)
|
|
34
|
+
if (redirectUrl.protocol !== 'http:' && redirectUrl.protocol !== 'https:') {
|
|
35
|
+
proxyRes.resume()
|
|
36
|
+
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
37
|
+
res.end(JSON.stringify({ error: 'Invalid redirect location' }))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const redirectModule = redirectUrl.protocol === 'https:' ? https : http
|
|
41
|
+
proxyRes.resume()
|
|
42
|
+
makeProxyRequest(redirectModule, redirectUrl, req, res, redirectCount + 1)
|
|
43
|
+
} catch {
|
|
44
|
+
proxyRes.resume()
|
|
45
|
+
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
46
|
+
res.end(JSON.stringify({ error: 'Invalid redirect location' }))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleProxyResponse(proxyRes, req, res, currentUrl, redirectCount) {
|
|
51
|
+
if (
|
|
52
|
+
proxyRes.statusCode >= 301 &&
|
|
53
|
+
proxyRes.statusCode <= 308 &&
|
|
54
|
+
proxyRes.headers.location &&
|
|
55
|
+
['GET', 'HEAD'].includes(req.method)
|
|
56
|
+
) {
|
|
57
|
+
handleRedirect(
|
|
58
|
+
proxyRes.headers.location,
|
|
59
|
+
req,
|
|
60
|
+
res,
|
|
61
|
+
proxyRes,
|
|
62
|
+
currentUrl,
|
|
63
|
+
redirectCount,
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const excludeHeaders = [
|
|
69
|
+
'connection',
|
|
70
|
+
'keep-alive',
|
|
71
|
+
'proxy-authenticate',
|
|
72
|
+
'proxy-authorization',
|
|
73
|
+
'te',
|
|
74
|
+
'trailers',
|
|
75
|
+
'transfer-encoding',
|
|
76
|
+
'upgrade',
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
Object.keys(proxyRes.headers).forEach((key) => {
|
|
80
|
+
if (!excludeHeaders.includes(key.toLowerCase())) {
|
|
81
|
+
res.setHeader(key, proxyRes.headers[key])
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
setCorsHeaders(res)
|
|
86
|
+
res.writeHead(proxyRes.statusCode)
|
|
87
|
+
proxyRes.pipe(res)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeProxyRequest(httpModule, parsedUrl, req, res, redirectCount = 0) {
|
|
91
|
+
const options = {
|
|
92
|
+
hostname: parsedUrl.hostname,
|
|
93
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
94
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
95
|
+
method: req.method,
|
|
96
|
+
headers: {
|
|
97
|
+
...req.headers,
|
|
98
|
+
host: parsedUrl.host,
|
|
99
|
+
},
|
|
100
|
+
rejectUnauthorized: false,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
delete options.headers['x-forwarded-for']
|
|
104
|
+
delete options.headers['x-forwarded-proto']
|
|
105
|
+
delete options.headers['x-forwarded-host']
|
|
106
|
+
|
|
107
|
+
const proxyReq = httpModule.request(options, (proxyRes) =>
|
|
108
|
+
handleProxyResponse(proxyRes, req, res, parsedUrl, redirectCount),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
proxyReq.on('error', (error) => {
|
|
112
|
+
console.error('Proxy error:', error.message)
|
|
113
|
+
if (!res.headersSent) {
|
|
114
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
115
|
+
res.end(JSON.stringify({ error: 'Proxy error', message: error.message }))
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
proxyReq.setTimeout(30000, () => {
|
|
120
|
+
proxyReq.destroy()
|
|
121
|
+
if (!res.headersSent) {
|
|
122
|
+
res.writeHead(504, { 'Content-Type': 'application/json' })
|
|
123
|
+
res.end(JSON.stringify({ error: 'Gateway timeout' }))
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
128
|
+
req.pipe(proxyReq)
|
|
129
|
+
} else {
|
|
130
|
+
proxyReq.end()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleProxyRequest(req, res) {
|
|
135
|
+
setCorsHeaders(res)
|
|
136
|
+
|
|
137
|
+
if (req.method === 'OPTIONS') {
|
|
138
|
+
res.writeHead(200)
|
|
139
|
+
res.end()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const urlMatch = req.url.match(/^\/(https?:\/\/.+)$/)
|
|
144
|
+
|
|
145
|
+
if (!urlMatch) {
|
|
146
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
147
|
+
res.end(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
error: 'Invalid request format. Expected: /{protocol}://{host}/{path}',
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const targetUrl = urlMatch[1]
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const parsedUrl = new URL(targetUrl)
|
|
159
|
+
console.log(`Proxying ${req.method} request to: ${targetUrl}`)
|
|
160
|
+
|
|
161
|
+
const httpModule = parsedUrl.protocol === 'https:' ? https : http
|
|
162
|
+
makeProxyRequest(httpModule, parsedUrl, req, res)
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Invalid URL:', targetUrl, error.message)
|
|
165
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
166
|
+
res.end(
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
error: 'Invalid URL format',
|
|
169
|
+
message: error.message,
|
|
170
|
+
}),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function start() {
|
|
176
|
+
const host = process.env.HOST || '0.0.0.0'
|
|
177
|
+
const port = process.env.PORT || 8080
|
|
178
|
+
|
|
179
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
180
|
+
|
|
181
|
+
const server = http.createServer((req, res) => {
|
|
182
|
+
handleProxyRequest(req, res)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
server.listen(port, host, () => {
|
|
186
|
+
console.log(
|
|
187
|
+
`Running CORS Proxy (Node.js built-ins) on http://${host}:${port}`,
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
@@ -21,15 +21,15 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
21
21
|
|
|
22
22
|
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git'])
|
|
23
23
|
|
|
24
|
-
function toTitleCase(kebab
|
|
24
|
+
function toTitleCase(kebab) {
|
|
25
25
|
return kebab
|
|
26
26
|
.split('-')
|
|
27
27
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
28
|
.join(' ')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function walkTextFiles(dir
|
|
32
|
-
const results
|
|
31
|
+
function walkTextFiles(dir) {
|
|
32
|
+
const results = []
|
|
33
33
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
34
|
const fullPath = path.join(dir, entry.name)
|
|
35
35
|
if (entry.isDirectory()) {
|
|
@@ -45,10 +45,7 @@ function walkTextFiles(dir: string): string[] {
|
|
|
45
45
|
return results
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function replaceInFile(
|
|
49
|
-
filePath: string,
|
|
50
|
-
replacements: Record<string, string>,
|
|
51
|
-
): void {
|
|
48
|
+
function replaceInFile(filePath, replacements) {
|
|
52
49
|
const original = fs.readFileSync(filePath, 'utf-8')
|
|
53
50
|
const updated = Object.entries(replacements).reduce(
|
|
54
51
|
(src, [placeholder, value]) => src.replaceAll(placeholder, value),
|
|
@@ -57,11 +54,11 @@ function replaceInFile(
|
|
|
57
54
|
if (updated !== original) fs.writeFileSync(filePath, updated, 'utf-8')
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
export async function createCommand(_args
|
|
57
|
+
export async function createCommand(_args) {
|
|
61
58
|
const projectRoot = process.cwd()
|
|
62
59
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
63
60
|
|
|
64
|
-
let pkg
|
|
61
|
+
let pkg
|
|
65
62
|
try {
|
|
66
63
|
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
67
64
|
} catch (error) {
|
|
@@ -84,7 +81,7 @@ export async function createCommand(_args: string[]) {
|
|
|
84
81
|
const appTitle = toTitleCase(appName)
|
|
85
82
|
const appDescription = `${appTitle} - Screenly Edge App`
|
|
86
83
|
|
|
87
|
-
const replacements
|
|
84
|
+
const replacements = {
|
|
88
85
|
'{{APP_NAME}}': appName,
|
|
89
86
|
'{{APP_TITLE}}': appTitle,
|
|
90
87
|
'{{APP_DESCRIPTION}}': appDescription,
|
|
@@ -107,12 +104,12 @@ Next steps:
|
|
|
107
104
|
1. Add an id field to screenly.yml and screenly_qc.yml.
|
|
108
105
|
|
|
109
106
|
2. Install dependencies:
|
|
110
|
-
|
|
107
|
+
npm install
|
|
111
108
|
|
|
112
109
|
3. Start the dev server:
|
|
113
|
-
|
|
110
|
+
npm run dev
|
|
114
111
|
|
|
115
112
|
4. Deploy when ready:
|
|
116
|
-
|
|
113
|
+
npm run deploy
|
|
117
114
|
`)
|
|
118
115
|
}
|
package/tsconfig.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"forceConsistentCasingInFileNames": true,
|
|
15
15
|
"resolveJsonModule": true,
|
|
16
16
|
"allowSyntheticDefaultImports": true,
|
|
17
|
-
"types": ["
|
|
17
|
+
"types": ["vite/client"]
|
|
18
18
|
},
|
|
19
19
|
"include": ["src/**/*"],
|
|
20
20
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
package/bin/edge-apps-scripts.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import http from 'http'
|
|
2
|
-
import https from 'https'
|
|
3
|
-
import { URL } from 'url'
|
|
4
|
-
|
|
5
|
-
// Listen on a specific host via the HOST environment variable
|
|
6
|
-
const host = process.env.HOST || '0.0.0.0'
|
|
7
|
-
// Listen on a specific port via the PORT environment variable
|
|
8
|
-
const port = process.env.PORT || 8080
|
|
9
|
-
|
|
10
|
-
// This script is for development purposes only and should never be deployed to production.
|
|
11
|
-
// Disabling TLS certificate validation is a critical security risk.
|
|
12
|
-
// Only use this in local development environments.
|
|
13
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
14
|
-
|
|
15
|
-
function setCorsHeaders(res: http.ServerResponse) {
|
|
16
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
17
|
-
res.setHeader(
|
|
18
|
-
'Access-Control-Allow-Methods',
|
|
19
|
-
'GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH',
|
|
20
|
-
)
|
|
21
|
-
res.setHeader('Access-Control-Allow-Headers', '*')
|
|
22
|
-
res.setHeader('Access-Control-Max-Age', '86400')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeProxyRequest(
|
|
26
|
-
httpModule: typeof http | typeof https,
|
|
27
|
-
parsedUrl: URL,
|
|
28
|
-
req: http.IncomingMessage,
|
|
29
|
-
res: http.ServerResponse,
|
|
30
|
-
) {
|
|
31
|
-
const options = {
|
|
32
|
-
hostname: parsedUrl.hostname,
|
|
33
|
-
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
34
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
35
|
-
method: req.method,
|
|
36
|
-
headers: {
|
|
37
|
-
...req.headers,
|
|
38
|
-
host: parsedUrl.host,
|
|
39
|
-
},
|
|
40
|
-
rejectUnauthorized: false,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
delete options.headers['x-forwarded-for']
|
|
44
|
-
delete options.headers['x-forwarded-proto']
|
|
45
|
-
delete options.headers['x-forwarded-host']
|
|
46
|
-
|
|
47
|
-
const proxyReq = httpModule.request(options, (proxyRes) => {
|
|
48
|
-
const excludeHeaders = [
|
|
49
|
-
'connection',
|
|
50
|
-
'keep-alive',
|
|
51
|
-
'proxy-authenticate',
|
|
52
|
-
'proxy-authorization',
|
|
53
|
-
'te',
|
|
54
|
-
'trailers',
|
|
55
|
-
'transfer-encoding',
|
|
56
|
-
'upgrade',
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
Object.keys(proxyRes.headers).forEach((key) => {
|
|
60
|
-
if (!excludeHeaders.includes(key.toLowerCase())) {
|
|
61
|
-
res.setHeader(key, proxyRes.headers[key])
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
res.writeHead(proxyRes.statusCode)
|
|
66
|
-
proxyRes.pipe(res)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
proxyReq.on('error', (error) => {
|
|
70
|
-
console.error('Proxy error:', error.message)
|
|
71
|
-
if (!res.headersSent) {
|
|
72
|
-
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
73
|
-
res.end(
|
|
74
|
-
JSON.stringify({
|
|
75
|
-
error: 'Proxy error',
|
|
76
|
-
message: error.message,
|
|
77
|
-
}),
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
proxyReq.setTimeout(30000, () => {
|
|
83
|
-
proxyReq.destroy()
|
|
84
|
-
if (!res.headersSent) {
|
|
85
|
-
res.writeHead(504, { 'Content-Type': 'application/json' })
|
|
86
|
-
res.end(
|
|
87
|
-
JSON.stringify({
|
|
88
|
-
error: 'Gateway timeout',
|
|
89
|
-
}),
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
95
|
-
req.pipe(proxyReq)
|
|
96
|
-
} else {
|
|
97
|
-
proxyReq.end()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function handleProxyRequest(
|
|
102
|
-
req: http.IncomingMessage,
|
|
103
|
-
res: http.ServerResponse,
|
|
104
|
-
) {
|
|
105
|
-
setCorsHeaders(res)
|
|
106
|
-
|
|
107
|
-
if (req.method === 'OPTIONS') {
|
|
108
|
-
res.writeHead(200)
|
|
109
|
-
res.end()
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const urlMatch = req.url.match(/^\/(https?:\/\/.+)$/)
|
|
114
|
-
|
|
115
|
-
if (!urlMatch) {
|
|
116
|
-
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
117
|
-
res.end(
|
|
118
|
-
JSON.stringify({
|
|
119
|
-
error: 'Invalid request format. Expected: /{protocol}://{host}/{path}',
|
|
120
|
-
}),
|
|
121
|
-
)
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const targetUrl = urlMatch[1]
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const parsedUrl = new URL(targetUrl)
|
|
129
|
-
console.log(`Proxying ${req.method} request to: ${targetUrl}`)
|
|
130
|
-
|
|
131
|
-
const httpModule = parsedUrl.protocol === 'https:' ? https : http
|
|
132
|
-
makeProxyRequest(httpModule, parsedUrl, req, res)
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error('Invalid URL:', targetUrl, error.message)
|
|
135
|
-
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
136
|
-
res.end(
|
|
137
|
-
JSON.stringify({
|
|
138
|
-
error: 'Invalid URL format',
|
|
139
|
-
message: error.message,
|
|
140
|
-
}),
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const server = http.createServer((req, res) => {
|
|
146
|
-
handleProxyRequest(req, res)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
// Start the server
|
|
150
|
-
server.listen(port, host, () => {
|
|
151
|
-
console.log(
|
|
152
|
-
`Running CORS Proxy (Node.js built-ins) on http://${host}:${port}`,
|
|
153
|
-
)
|
|
154
|
-
})
|