@screenly/edge-apps 0.0.3 → 0.0.5
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/package.json +3 -1
- package/scripts/cli.ts +14 -14
- package/vite-plugins/dev-server.ts +191 -0
- package/vite.config.ts +70 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@screenly/edge-apps",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A TypeScript library for interfacing with Screenly Edge Apps API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"bin",
|
|
47
47
|
"configs",
|
|
48
48
|
"scripts",
|
|
49
|
+
"vite-plugins",
|
|
49
50
|
"eslint.config.ts",
|
|
51
|
+
"vite.config.ts",
|
|
50
52
|
"src/styles",
|
|
51
53
|
"src/assets"
|
|
52
54
|
],
|
package/scripts/cli.ts
CHANGED
|
@@ -45,6 +45,16 @@ const commands = {
|
|
|
45
45
|
},
|
|
46
46
|
}
|
|
47
47
|
|
|
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 {
|
|
52
|
+
const appBin = path.resolve(process.cwd(), 'node_modules', '.bin', name)
|
|
53
|
+
return fs.existsSync(appBin)
|
|
54
|
+
? appBin
|
|
55
|
+
: path.resolve(libraryRoot, 'node_modules', '.bin', name)
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
59
|
* Helper: Get NODE_PATH with library's node_modules included
|
|
50
60
|
*/
|
|
@@ -99,19 +109,14 @@ function spawnWithSignalHandling(
|
|
|
99
109
|
*/
|
|
100
110
|
function getVitePaths(): { viteBin: string; configPath: string } {
|
|
101
111
|
return {
|
|
102
|
-
viteBin:
|
|
112
|
+
viteBin: resolveBin('vite'),
|
|
103
113
|
configPath: path.resolve(libraryRoot, 'vite.config.ts'),
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
async function lintCommand(args: string[]) {
|
|
108
118
|
try {
|
|
109
|
-
const eslintBin =
|
|
110
|
-
libraryRoot,
|
|
111
|
-
'node_modules',
|
|
112
|
-
'.bin',
|
|
113
|
-
'eslint',
|
|
114
|
-
)
|
|
119
|
+
const eslintBin = resolveBin('eslint')
|
|
115
120
|
|
|
116
121
|
const eslintArgs = [
|
|
117
122
|
'--config',
|
|
@@ -181,7 +186,7 @@ async function buildDevCommand(args: string[]) {
|
|
|
181
186
|
|
|
182
187
|
async function typeCheckCommand(args: string[]) {
|
|
183
188
|
try {
|
|
184
|
-
const tscBin =
|
|
189
|
+
const tscBin = resolveBin('tsc')
|
|
185
190
|
|
|
186
191
|
const tscArgs = [
|
|
187
192
|
'--noEmit',
|
|
@@ -215,12 +220,7 @@ async function convertPngsToWebP(screenshotsDir: string): Promise<void> {
|
|
|
215
220
|
|
|
216
221
|
async function screenshotsCommand(_args: string[]) {
|
|
217
222
|
try {
|
|
218
|
-
const playwrightBin =
|
|
219
|
-
process.cwd(),
|
|
220
|
-
'node_modules',
|
|
221
|
-
'.bin',
|
|
222
|
-
'playwright',
|
|
223
|
-
)
|
|
223
|
+
const playwrightBin = resolveBin('playwright')
|
|
224
224
|
const playwrightConfig = path.resolve(
|
|
225
225
|
libraryRoot,
|
|
226
226
|
'configs',
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ViteDevServer, Plugin } from 'vite'
|
|
2
|
+
import YAML from 'yaml'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
type ScreenlyManifestField = {
|
|
7
|
+
type: string
|
|
8
|
+
default_value?: string
|
|
9
|
+
title: string
|
|
10
|
+
optional: boolean
|
|
11
|
+
is_global?: boolean
|
|
12
|
+
help_text: string | Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type BaseScreenlyMockData = {
|
|
16
|
+
metadata: {
|
|
17
|
+
coordinates: [number, number]
|
|
18
|
+
hostname: string
|
|
19
|
+
screen_name: string
|
|
20
|
+
hardware: string
|
|
21
|
+
location: string
|
|
22
|
+
screenly_version: string
|
|
23
|
+
tags: string[]
|
|
24
|
+
}
|
|
25
|
+
settings: Record<string, string>
|
|
26
|
+
cors_proxy_url: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultScreenlyConfig: BaseScreenlyMockData = {
|
|
30
|
+
metadata: {
|
|
31
|
+
coordinates: [37.3861, -122.0839] as [number, number],
|
|
32
|
+
hostname: 'dev-hostname',
|
|
33
|
+
screen_name: 'Development Server',
|
|
34
|
+
hardware: 'x86',
|
|
35
|
+
location: 'Development Environment',
|
|
36
|
+
screenly_version: 'development-server',
|
|
37
|
+
tags: ['Development'],
|
|
38
|
+
},
|
|
39
|
+
settings: {
|
|
40
|
+
enable_analytics: 'true',
|
|
41
|
+
tag_manager_id: '',
|
|
42
|
+
theme: 'light',
|
|
43
|
+
screenly_color_accent: '#972EFF',
|
|
44
|
+
screenly_color_light: '#ADAFBE',
|
|
45
|
+
screenly_color_dark: '#454BD2',
|
|
46
|
+
},
|
|
47
|
+
cors_proxy_url: 'http://127.0.0.1:8080',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateScreenlyObject(config: BaseScreenlyMockData) {
|
|
51
|
+
return `
|
|
52
|
+
// Generated screenly.js for development mode
|
|
53
|
+
window.screenly = {
|
|
54
|
+
signalReadyForRendering: () => {},
|
|
55
|
+
metadata: ${JSON.stringify(config.metadata, null, 2)},
|
|
56
|
+
settings: ${JSON.stringify(config.settings, null, 2)},
|
|
57
|
+
cors_proxy_url: ${JSON.stringify(config.cors_proxy_url)}
|
|
58
|
+
}
|
|
59
|
+
`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function generateMockData(
|
|
63
|
+
rootDir: string,
|
|
64
|
+
previousConfig: BaseScreenlyMockData = defaultScreenlyConfig,
|
|
65
|
+
): BaseScreenlyMockData {
|
|
66
|
+
const manifestPath = path.resolve(rootDir, 'screenly.yml')
|
|
67
|
+
const mockDataPath = path.resolve(rootDir, 'mock-data.yml')
|
|
68
|
+
|
|
69
|
+
let manifest: Record<string, unknown>
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(manifestPath)) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`screenly.yml not found at ${manifestPath}, using previous config.`,
|
|
74
|
+
)
|
|
75
|
+
return previousConfig
|
|
76
|
+
}
|
|
77
|
+
manifest = YAML.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
80
|
+
console.warn(
|
|
81
|
+
`Failed to parse screenly.yml: ${message}. Using previous config.`,
|
|
82
|
+
)
|
|
83
|
+
return previousConfig
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const screenlyConfig: BaseScreenlyMockData = structuredClone(
|
|
87
|
+
defaultScreenlyConfig,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Merge settings from manifest
|
|
91
|
+
if (manifest?.settings && typeof manifest.settings === 'object') {
|
|
92
|
+
for (const [key, value] of Object.entries(manifest.settings) as [
|
|
93
|
+
string,
|
|
94
|
+
ScreenlyManifestField,
|
|
95
|
+
][]) {
|
|
96
|
+
if (value.type === 'string' || value.type === 'secret') {
|
|
97
|
+
const manifestField: ScreenlyManifestField = value
|
|
98
|
+
const defaultValue = manifestField?.default_value ?? ''
|
|
99
|
+
screenlyConfig.settings[key] = defaultValue
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
applyMockDataOverrides(mockDataPath, screenlyConfig)
|
|
105
|
+
|
|
106
|
+
return screenlyConfig
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyMockDataOverrides(
|
|
110
|
+
mockDataPath: string,
|
|
111
|
+
screenlyConfig: BaseScreenlyMockData,
|
|
112
|
+
): void {
|
|
113
|
+
if (!fs.existsSync(mockDataPath)) return
|
|
114
|
+
|
|
115
|
+
// Typed as unknown since YAML.parse() can return any value (string, array, null, etc.)
|
|
116
|
+
let parsedMockData: unknown
|
|
117
|
+
try {
|
|
118
|
+
parsedMockData = YAML.parse(fs.readFileSync(mockDataPath, 'utf8'))
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
121
|
+
console.warn(
|
|
122
|
+
`Failed to parse mock-data.yml: ${message}. Keeping config without mock-data overrides.`,
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Narrow to a plain object after ruling out null, primitives, and arrays
|
|
128
|
+
if (
|
|
129
|
+
!parsedMockData ||
|
|
130
|
+
typeof parsedMockData !== 'object' ||
|
|
131
|
+
Array.isArray(parsedMockData)
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
const mockData = parsedMockData as Record<string, unknown>
|
|
136
|
+
|
|
137
|
+
const { metadata, settings, cors_proxy_url } = mockData
|
|
138
|
+
|
|
139
|
+
if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {
|
|
140
|
+
Object.assign(screenlyConfig.metadata, metadata)
|
|
141
|
+
}
|
|
142
|
+
if (settings && typeof settings === 'object' && !Array.isArray(settings)) {
|
|
143
|
+
Object.assign(screenlyConfig.settings, settings)
|
|
144
|
+
}
|
|
145
|
+
if (typeof cors_proxy_url === 'string' && cors_proxy_url.length > 0) {
|
|
146
|
+
screenlyConfig.cors_proxy_url = cors_proxy_url
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function screenlyDevServer(): Plugin {
|
|
151
|
+
let config: BaseScreenlyMockData
|
|
152
|
+
let rootDir: string
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: 'screenly-dev-server',
|
|
156
|
+
configureServer(server: ViteDevServer) {
|
|
157
|
+
rootDir = server.config.root
|
|
158
|
+
|
|
159
|
+
// Generate initial mock data
|
|
160
|
+
config = generateMockData(rootDir)
|
|
161
|
+
|
|
162
|
+
// Watch for changes to screenly.yml and mock-data.yml using Vite's watcher
|
|
163
|
+
const manifestPath = path.resolve(rootDir, 'screenly.yml')
|
|
164
|
+
const mockDataPath = path.resolve(rootDir, 'mock-data.yml')
|
|
165
|
+
|
|
166
|
+
const handleConfigFileChange = (file: string) => {
|
|
167
|
+
if (file === manifestPath || file === mockDataPath) {
|
|
168
|
+
config = generateMockData(rootDir, config)
|
|
169
|
+
// Notify connected clients to perform a full reload so they pick up the new mock config
|
|
170
|
+
server.ws.send({ type: 'full-reload', path: '*' })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
server.watcher.add([manifestPath, mockDataPath])
|
|
175
|
+
server.watcher.on('add', handleConfigFileChange)
|
|
176
|
+
server.watcher.on('change', handleConfigFileChange)
|
|
177
|
+
server.watcher.on('unlink', handleConfigFileChange)
|
|
178
|
+
|
|
179
|
+
server.middlewares.use((req, res, next) => {
|
|
180
|
+
if (req.url === '/screenly.js?version=1') {
|
|
181
|
+
const screenlyJsContent = generateScreenlyObject(config)
|
|
182
|
+
|
|
183
|
+
res.setHeader('Content-Type', 'application/javascript')
|
|
184
|
+
res.end(screenlyJsContent)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
next()
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineConfig, Plugin } from 'vite'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
import { copyFileSync, existsSync } from 'fs'
|
|
4
|
+
import { resolve, dirname } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { screenlyDevServer } from './vite-plugins/dev-server'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = dirname(__filename)
|
|
10
|
+
|
|
11
|
+
function copyScreenlyFiles(): Plugin {
|
|
12
|
+
return {
|
|
13
|
+
name: 'copy-screenly-files',
|
|
14
|
+
closeBundle() {
|
|
15
|
+
const filesToCopy = ['screenly.yml', 'screenly_qc.yml', 'instance.yml']
|
|
16
|
+
|
|
17
|
+
for (const file of filesToCopy) {
|
|
18
|
+
const srcPath = resolve(process.cwd(), file)
|
|
19
|
+
if (existsSync(srcPath)) {
|
|
20
|
+
const destPath = resolve(process.cwd(), 'dist', file)
|
|
21
|
+
try {
|
|
22
|
+
copyFileSync(srcPath, destPath)
|
|
23
|
+
console.log(`Copied ${file} to dist/`)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message =
|
|
26
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Failed to copy "${file}" to dist/: ${message}`,
|
|
29
|
+
{ cause: error },
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
base: '',
|
|
40
|
+
server: {
|
|
41
|
+
fs: {
|
|
42
|
+
// Allow serving files from both the app directory and the library directory
|
|
43
|
+
allow: [
|
|
44
|
+
process.cwd(), // The edge app directory (e.g., edge-apps/clock)
|
|
45
|
+
resolve(__dirname), // The edge-apps-library directory
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
build: {
|
|
50
|
+
cssCodeSplit: false,
|
|
51
|
+
assetsInlineLimit: 7000000,
|
|
52
|
+
minify: true,
|
|
53
|
+
rollupOptions: {
|
|
54
|
+
input: 'index.html',
|
|
55
|
+
output: {
|
|
56
|
+
dir: 'dist',
|
|
57
|
+
entryFileNames: 'js/[name].js',
|
|
58
|
+
format: 'iife',
|
|
59
|
+
assetFileNames: (assetInfo) => {
|
|
60
|
+
if (assetInfo.names?.[0]?.endsWith('.css')) {
|
|
61
|
+
return 'css/style.css'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 'assets/[name]-[hash][extname]'
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
plugins: [tailwindcss(), screenlyDevServer(), copyScreenlyFiles()],
|
|
70
|
+
})
|