@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 CHANGED
@@ -6,8 +6,14 @@ A TypeScript library for interfacing with the Screenly Edge Apps API.
6
6
 
7
7
  ## Installation
8
8
 
9
- > [!IMPORTANT]
10
- > This package requires [Bun](https://bun.sh). The `edge-apps-scripts` CLI will not work with `npm` or `node` at this time.
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
- bun link
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
- bun link @screenly/edge-apps
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
- bun install --force
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/master/docs/components.md) for usage details.
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
- bun run dev
115
+ npm run dev
114
116
  ```
115
117
 
116
118
  #### Building
117
119
 
118
120
  ```bash
119
- bun run build
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
- bun run lint
129
+ npm run lint
128
130
  ```
129
131
 
130
132
  To lint and automatically fix issues:
131
133
 
132
134
  ```bash
133
- bun run lint -- --fix
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
- bun run type-check
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": "bun run build && screenly edge-app deploy --path=dist/"
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
- bun install # Install dependencies
201
- bun test # Run tests
202
- bun run build # Build library
202
+ npm install # Install dependencies
203
+ npm test # Run tests
204
+ npm run build # Build library
203
205
  ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../scripts/cli.js'
4
+
5
+ run()
@@ -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. 'bun add -d @playwright/test') " +
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.1.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.ts"
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": "bun run build",
59
- "prebuild": "bun run type-check",
60
- "build": "bun build:types && bun run copy:assets",
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": "bun run scripts/copy-assets.ts",
63
- "test": "bun test --parallel",
64
- "test:unit": "bun test --parallel",
65
- "test:coverage": "bun test --coverage --parallel",
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
- "bun-types": "^1.3.10",
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 bun
2
- /**
3
- * CLI command dispatcher for edge-apps-scripts
4
- */
1
+ #!/usr/bin/env node
5
2
 
6
- import { execSync, execFileSync, spawn, type ChildProcess } from 'child_process'
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
- * Helper: Setup signal handlers for spawned processes
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: string[]) {
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: string[]) {
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: string[]) {
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: string[]) {
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: string[]) {
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: string): Promise<void> {
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: string[]) {
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 as keyof typeof commands].handler
266
+ const handler = commands[cmd].handler
284
267
  await handler(cmdArgs)
285
268
  }
286
269
 
@@ -1,7 +1,4 @@
1
- #!/usr/bin/env bun
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: string): string {
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: string): string[] {
32
- const results: string[] = []
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: string[]) {
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: Record<string, unknown>
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: Record<string, string> = {
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
- bun install
107
+ npm install
111
108
 
112
109
  3. Start the dev server:
113
- bun run dev
110
+ npm run dev
114
111
 
115
112
  4. Deploy when ready:
116
- bun run deploy
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": ["bun-types", "vite/client"]
17
+ "types": ["vite/client"]
18
18
  },
19
19
  "include": ["src/**/*"],
20
20
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Edge Apps Scripts CLI
4
- * Wrapper around build tools for Edge Apps
5
- */
6
-
7
- import { run } from '../scripts/cli'
8
-
9
- run()
@@ -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
- })