@screenly/edge-apps 1.1.2 → 1.2.1
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 +43 -0
- package/dist/components/app-header/app-header.js +1 -1
- package/package.json +1 -1
- package/scripts/cli.js +1 -1
- package/scripts/create-scaffold.js +234 -0
- package/scripts/create-template/.prettierrc.json +6 -0
- package/scripts/create-template/README.md +43 -0
- package/scripts/create-template/_gitignore +5 -0
- package/scripts/create-template/e2e/screenshots.spec.ts +33 -0
- package/scripts/create-template/index.html +24 -0
- package/scripts/create-template/package.json +27 -0
- package/scripts/create-template/screenly.yml +17 -0
- package/scripts/create-template/src/main.ts +18 -0
- package/scripts/create-template/src/style.css +30 -0
- package/scripts/create-template/tsconfig.json +8 -0
- package/scripts/create-template/vitest.config.ts +7 -0
- package/scripts/create.js +42 -56
- package/scripts/template-utils.js +65 -0
package/README.md
CHANGED
|
@@ -19,6 +19,49 @@ Or with Bun:
|
|
|
19
19
|
bun add @screenly/edge-apps
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Creating a New Edge App
|
|
23
|
+
|
|
24
|
+
Scaffold a new Edge App without installing anything first:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @screenly/edge-apps create my-edge-app
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with Bun:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bunx @screenly/edge-apps create my-edge-app
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The package manager used to invoke the command is detected automatically, so
|
|
37
|
+
the generated `package.json` scripts are wired up to match (`npm run ...` vs.
|
|
38
|
+
`bun run ...`). This generates a minimal Edge App with a `screenly.yml`
|
|
39
|
+
manifest, `index.html`, and a `src/main.ts` entry point wired up to this
|
|
40
|
+
library.
|
|
41
|
+
|
|
42
|
+
Pass options after the directory name, for example:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @screenly/edge-apps create my-edge-app --description "My Edge App" --author "Jane Doe"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Option | Description |
|
|
49
|
+
| ---------------------- | ------------------------------------------------------------------- |
|
|
50
|
+
| `--description <text>` | Description used in `package.json`, `screenly.yml`, and `README.md` |
|
|
51
|
+
| `--author <text>` | Author name added to `package.json` |
|
|
52
|
+
| `--pm <npm\|bun>` | Force a package manager instead of auto-detecting it |
|
|
53
|
+
| `--force` | Write into an existing, non-empty directory |
|
|
54
|
+
| `--skip-install` | Skip installing dependencies after scaffolding |
|
|
55
|
+
|
|
56
|
+
> [!NOTE]
|
|
57
|
+
> Running `edge-apps-scripts create` with no directory argument instead
|
|
58
|
+
> replaces `{{APP_NAME}}`-style placeholders in the current project — this is
|
|
59
|
+
> used by Edge App template repositories after cloning, and is unrelated to
|
|
60
|
+
> scaffolding a brand new app.
|
|
61
|
+
|
|
62
|
+
No test files are included — `test`/`test:unit` work out of the box on an
|
|
63
|
+
empty suite; add your own under `src/` when you have something to test.
|
|
64
|
+
|
|
22
65
|
### Local Development Setup
|
|
23
66
|
|
|
24
67
|
When developing Edge Apps locally using the library from this repository, you should link the package.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatTime, formatLocalizedDate, getTimeZone, getLocale, } from '../../utils/index.js';
|
|
2
2
|
const htmlTemplate = `
|
|
3
3
|
<div class="header-left">
|
|
4
|
-
<brand-logo
|
|
4
|
+
<brand-logo></brand-logo>
|
|
5
5
|
</div>
|
|
6
6
|
<div class="header-right">
|
|
7
7
|
<span class="header-date"></span>
|
package/package.json
CHANGED
package/scripts/cli.js
CHANGED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { spawnSync } from 'child_process'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import {
|
|
6
|
+
toKebabCase,
|
|
7
|
+
toTitleCase,
|
|
8
|
+
walkTextFiles,
|
|
9
|
+
replaceInFile,
|
|
10
|
+
} from './template-utils.js'
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const libraryRoot = path.resolve(__dirname, '..')
|
|
14
|
+
const templateRoot = path.resolve(__dirname, 'create-template')
|
|
15
|
+
const libraryPkg = JSON.parse(
|
|
16
|
+
fs.readFileSync(path.join(libraryRoot, 'package.json'), 'utf-8'),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const NPM_RUN_ALL2_VERSION = '^8.0.4'
|
|
20
|
+
const TYPES_BUN_VERSION = '^1.3.13'
|
|
21
|
+
const BUN_TYPES_VERSION = '^1.3.13'
|
|
22
|
+
|
|
23
|
+
const VALUE_FLAGS = new Set(['--pm', '--description', '--author'])
|
|
24
|
+
const BOOLEAN_FLAGS = new Set(['--force', '--skip-install'])
|
|
25
|
+
|
|
26
|
+
export function parseCreateArgs(args) {
|
|
27
|
+
const options = {
|
|
28
|
+
pm: null,
|
|
29
|
+
description: null,
|
|
30
|
+
author: null,
|
|
31
|
+
force: false,
|
|
32
|
+
skipInstall: false,
|
|
33
|
+
}
|
|
34
|
+
const positional = []
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i]
|
|
38
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
39
|
+
const value = args[i + 1]
|
|
40
|
+
if (value === undefined || value.startsWith('-')) {
|
|
41
|
+
return { error: `Missing value for ${arg}` }
|
|
42
|
+
}
|
|
43
|
+
i++
|
|
44
|
+
if (arg === '--pm') options.pm = value
|
|
45
|
+
else if (arg === '--description') options.description = value
|
|
46
|
+
else options.author = value
|
|
47
|
+
} else if (BOOLEAN_FLAGS.has(arg)) {
|
|
48
|
+
if (arg === '--force') options.force = true
|
|
49
|
+
else options.skipInstall = true
|
|
50
|
+
} else if (arg.startsWith('-')) {
|
|
51
|
+
return { error: `Unknown option: ${arg}` }
|
|
52
|
+
} else {
|
|
53
|
+
positional.push(arg)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (positional.length > 1) {
|
|
58
|
+
return {
|
|
59
|
+
error: `Expected a single directory argument, got: ${positional.join(', ')}`,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { directory: positional[0], options }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateDescription(description) {
|
|
67
|
+
if (description !== null && /[\r\n]/.test(description)) {
|
|
68
|
+
return 'Description cannot contain line breaks.'
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectPackageManager(explicit) {
|
|
74
|
+
if (explicit) {
|
|
75
|
+
if (explicit !== 'npm' && explicit !== 'bun') {
|
|
76
|
+
console.error(
|
|
77
|
+
`Unsupported package manager: ${explicit}. Use "npm" or "bun".`,
|
|
78
|
+
)
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
return explicit
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const userAgent = process.env.npm_config_user_agent || ''
|
|
85
|
+
return userAgent.startsWith('bun') ? 'bun' : 'npm'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function finalizePackageJson(destination, pm) {
|
|
89
|
+
const pkgPath = path.join(destination, 'package.json')
|
|
90
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
91
|
+
|
|
92
|
+
pkg.devDependencies = {
|
|
93
|
+
...pkg.devDependencies,
|
|
94
|
+
'@screenly/edge-apps': `^${libraryPkg.version}`,
|
|
95
|
+
typescript: libraryPkg.dependencies.typescript,
|
|
96
|
+
prettier: libraryPkg.devDependencies.prettier,
|
|
97
|
+
'@types/node': libraryPkg.devDependencies['@types/node'],
|
|
98
|
+
'npm-run-all2': NPM_RUN_ALL2_VERSION,
|
|
99
|
+
'@playwright/test': libraryPkg.peerDependencies['@playwright/test'],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pm === 'bun') {
|
|
103
|
+
pkg.scripts.test = 'bun test --pass-with-no-tests src/'
|
|
104
|
+
pkg.devDependencies['@types/bun'] = TYPES_BUN_VERSION
|
|
105
|
+
pkg.devDependencies['bun-types'] = BUN_TYPES_VERSION
|
|
106
|
+
pkg.devDependencies.jsdom = libraryPkg.dependencies.jsdom
|
|
107
|
+
pkg.devDependencies['@types/jsdom'] =
|
|
108
|
+
libraryPkg.devDependencies['@types/jsdom']
|
|
109
|
+
} else {
|
|
110
|
+
pkg.scripts.test = 'vitest run --passWithNoTests'
|
|
111
|
+
pkg.devDependencies.vitest = libraryPkg.devDependencies.vitest
|
|
112
|
+
pkg.devDependencies.jsdom = libraryPkg.dependencies.jsdom
|
|
113
|
+
}
|
|
114
|
+
pkg.scripts['test:unit'] = pkg.scripts.test
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installDependencies(destination, pm) {
|
|
120
|
+
console.log(`\nInstalling dependencies with ${pm}...`)
|
|
121
|
+
const result = spawnSync(pm, ['install'], {
|
|
122
|
+
cwd: destination,
|
|
123
|
+
stdio: 'inherit',
|
|
124
|
+
shell: process.platform === 'win32',
|
|
125
|
+
})
|
|
126
|
+
if (result.status !== 0) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`\n${pm} install failed. Run it manually inside ${destination}.`,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printScaffoldNextSteps(destination, appName, pm) {
|
|
134
|
+
const relativePath = path.relative(process.cwd(), destination) || '.'
|
|
135
|
+
const runCommand = pm === 'bun' ? 'bun run' : 'npm run'
|
|
136
|
+
const installCommand = pm === 'bun' ? 'bun install' : 'npm install'
|
|
137
|
+
const needsInstall = !fs.existsSync(path.join(destination, 'node_modules'))
|
|
138
|
+
|
|
139
|
+
const steps = [`cd "${relativePath}"`]
|
|
140
|
+
if (needsInstall) steps.push(installCommand)
|
|
141
|
+
steps.push(
|
|
142
|
+
`Register the app to get an id for screenly.yml:\n screenly edge-app create --name ${appName} --in-place`,
|
|
143
|
+
`Start the dev server:\n ${runCommand} dev`,
|
|
144
|
+
`Deploy when ready:\n ${runCommand} deploy`,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
console.log(`
|
|
148
|
+
Done! Your Edge App is ready.
|
|
149
|
+
|
|
150
|
+
Next steps:
|
|
151
|
+
${steps.map((step, index) => ` ${index + 1}. ${step}`).join('\n')}
|
|
152
|
+
`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function scaffoldNewApp(directory, options) {
|
|
156
|
+
const pm = detectPackageManager(options.pm)
|
|
157
|
+
if (!pm) {
|
|
158
|
+
process.exitCode = 1
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const descriptionError = validateDescription(options.description)
|
|
163
|
+
if (descriptionError) {
|
|
164
|
+
console.error(descriptionError)
|
|
165
|
+
process.exitCode = 1
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const destination = path.resolve(process.cwd(), directory)
|
|
170
|
+
|
|
171
|
+
if (fs.existsSync(destination)) {
|
|
172
|
+
if (!fs.statSync(destination).isDirectory()) {
|
|
173
|
+
console.error(`"${directory}" already exists and is not a directory.`)
|
|
174
|
+
process.exitCode = 1
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
const isEmpty = fs.readdirSync(destination).length === 0
|
|
178
|
+
if (!isEmpty && !options.force) {
|
|
179
|
+
console.error(
|
|
180
|
+
`Directory "${directory}" already exists and is not empty. Use --force to write into it anyway.`,
|
|
181
|
+
)
|
|
182
|
+
process.exitCode = 1
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
fs.mkdirSync(destination, { recursive: true })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const appName = toKebabCase(path.basename(destination))
|
|
190
|
+
const appTitle = toTitleCase(appName)
|
|
191
|
+
const appDescription =
|
|
192
|
+
options.description ?? `${appTitle} - Screenly Edge App`
|
|
193
|
+
|
|
194
|
+
console.log(`\nScaffolding a new Edge App in ${destination}`)
|
|
195
|
+
|
|
196
|
+
fs.cpSync(templateRoot, destination, { recursive: true })
|
|
197
|
+
fs.copyFileSync(
|
|
198
|
+
path.join(destination, '_gitignore'),
|
|
199
|
+
path.join(destination, '.gitignore'),
|
|
200
|
+
)
|
|
201
|
+
fs.rmSync(path.join(destination, '_gitignore'))
|
|
202
|
+
|
|
203
|
+
const replacements = {
|
|
204
|
+
'{{APP_NAME}}': appName,
|
|
205
|
+
'{{APP_TITLE}}': appTitle,
|
|
206
|
+
'{{APP_DESCRIPTION}}': appDescription,
|
|
207
|
+
'{{APP_DESCRIPTION_JSON}}': JSON.stringify(appDescription).slice(1, -1),
|
|
208
|
+
'{{APP_DESCRIPTION_YAML}}': appDescription.replace(/'/g, "''"),
|
|
209
|
+
'{{PM_RUN}}': pm === 'bun' ? 'bun run' : 'npm run',
|
|
210
|
+
'{{PM_INSTALL}}': pm === 'bun' ? 'bun install' : 'npm install',
|
|
211
|
+
}
|
|
212
|
+
for (const filePath of walkTextFiles(destination)) {
|
|
213
|
+
replaceInFile(filePath, replacements)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
finalizePackageJson(destination, pm)
|
|
217
|
+
|
|
218
|
+
if (options.author) {
|
|
219
|
+
const pkgPath = path.join(destination, 'package.json')
|
|
220
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
221
|
+
pkg.author = options.author
|
|
222
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (pm === 'bun') {
|
|
226
|
+
fs.rmSync(path.join(destination, 'vitest.config.ts'), { force: true })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!options.skipInstall) {
|
|
230
|
+
installDependencies(destination, pm)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
printScaffoldNextSteps(destination, appName, pm)
|
|
234
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# {{APP_TITLE}}
|
|
2
|
+
|
|
3
|
+
{{APP_DESCRIPTION}}
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
Install dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
{{PM_INSTALL}}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Development
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
{{PM_RUN}} dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Build
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
{{PM_RUN}} build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Deployment
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
screenly edge-app create --name {{APP_NAME}} --in-place
|
|
29
|
+
{{PM_RUN}} deploy
|
|
30
|
+
screenly edge-app instance create
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
| Setting | Description | Required | Default |
|
|
36
|
+
| --------- | ------------------------------- | -------- | ------------------ |
|
|
37
|
+
| `message` | The message displayed on screen | No | `Hello, Screenly!` |
|
|
38
|
+
|
|
39
|
+
## Screenshots
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
{{PM_RUN}} screenshots
|
|
43
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
import {
|
|
3
|
+
createMockScreenlyForScreenshots,
|
|
4
|
+
getScreenshotsDir,
|
|
5
|
+
RESOLUTIONS,
|
|
6
|
+
setupClockMock,
|
|
7
|
+
setupScreenlyJsMock,
|
|
8
|
+
} from '@screenly/edge-apps/test/screenshots'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
|
|
11
|
+
const { screenlyJsContent } = createMockScreenlyForScreenshots()
|
|
12
|
+
|
|
13
|
+
for (const { width, height } of RESOLUTIONS) {
|
|
14
|
+
test(`screenshot ${width}x${height}`, async ({ browser }) => {
|
|
15
|
+
const screenshotsDir = getScreenshotsDir()
|
|
16
|
+
|
|
17
|
+
const context = await browser.newContext({ viewport: { width, height } })
|
|
18
|
+
const page = await context.newPage()
|
|
19
|
+
|
|
20
|
+
await setupClockMock(page)
|
|
21
|
+
await setupScreenlyJsMock(page, screenlyJsContent)
|
|
22
|
+
|
|
23
|
+
await page.goto('/')
|
|
24
|
+
await page.waitForLoadState('networkidle')
|
|
25
|
+
|
|
26
|
+
await page.screenshot({
|
|
27
|
+
path: path.join(screenshotsDir, `${width}x${height}.png`),
|
|
28
|
+
fullPage: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await context.close()
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{APP_TITLE}} - Screenly Edge App</title>
|
|
7
|
+
<script src="screenly.js?version=1"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<auto-scaler
|
|
11
|
+
reference-width="1920"
|
|
12
|
+
reference-height="1080"
|
|
13
|
+
orientation="auto"
|
|
14
|
+
>
|
|
15
|
+
<div id="app">
|
|
16
|
+
<app-header show-date></app-header>
|
|
17
|
+
<main class="content">
|
|
18
|
+
<h1 id="message"></h1>
|
|
19
|
+
</main>
|
|
20
|
+
</div>
|
|
21
|
+
</auto-scaler>
|
|
22
|
+
<script type="module" src="/src/main.ts"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{APP_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "{{APP_DESCRIPTION_JSON}}",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prebuild": "{{PM_RUN}} type-check",
|
|
8
|
+
"generate-mock-data": "screenly edge-app run --generate-mock-data",
|
|
9
|
+
"predev": "{{PM_RUN}} generate-mock-data",
|
|
10
|
+
"cors-proxy-server": "edge-apps-scripts cors-proxy",
|
|
11
|
+
"dev": "run-p cors-proxy-server edge-apps-dev",
|
|
12
|
+
"edge-apps-dev": "edge-apps-scripts dev",
|
|
13
|
+
"build": "edge-apps-scripts build",
|
|
14
|
+
"build:dev": "edge-apps-scripts build:dev",
|
|
15
|
+
"build:prod": "edge-apps-scripts build",
|
|
16
|
+
"test": "",
|
|
17
|
+
"test:unit": "",
|
|
18
|
+
"lint": "edge-apps-scripts lint --fix",
|
|
19
|
+
"format": "prettier --write src/ README.md index.html",
|
|
20
|
+
"format:check": "prettier --check src/ README.md index.html",
|
|
21
|
+
"deploy": "{{PM_RUN}} build && screenly edge-app deploy --path=dist/",
|
|
22
|
+
"type-check": "edge-apps-scripts type-check",
|
|
23
|
+
"screenshots": "edge-apps-scripts screenshots"
|
|
24
|
+
},
|
|
25
|
+
"prettier": "./.prettierrc.json",
|
|
26
|
+
"devDependencies": {}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
syntax: manifest_v1
|
|
3
|
+
description: '{{APP_DESCRIPTION_YAML}}'
|
|
4
|
+
ready_signal: true
|
|
5
|
+
settings:
|
|
6
|
+
message:
|
|
7
|
+
type: string
|
|
8
|
+
default_value: 'Hello, Screenly!'
|
|
9
|
+
title: Message
|
|
10
|
+
optional: true
|
|
11
|
+
help_text: The message displayed on screen.
|
|
12
|
+
sentry_dsn:
|
|
13
|
+
type: secret
|
|
14
|
+
title: Sentry Client Key
|
|
15
|
+
optional: true
|
|
16
|
+
help_text: Sentry Client Key from Sentry SDK for error capturing.
|
|
17
|
+
is_global: true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './style.css'
|
|
2
|
+
import '@screenly/edge-apps/components'
|
|
3
|
+
import {
|
|
4
|
+
getSettingWithDefault,
|
|
5
|
+
setupErrorHandling,
|
|
6
|
+
setupTheme,
|
|
7
|
+
signalReady,
|
|
8
|
+
} from '@screenly/edge-apps'
|
|
9
|
+
|
|
10
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
11
|
+
setupErrorHandling()
|
|
12
|
+
setupTheme()
|
|
13
|
+
|
|
14
|
+
const message = getSettingWithDefault<string>('message', 'Hello, Screenly!')
|
|
15
|
+
document.getElementById('message')!.textContent = message
|
|
16
|
+
|
|
17
|
+
signalReady()
|
|
18
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import '@screenly/edge-apps/styles';
|
|
2
|
+
|
|
3
|
+
* {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#app {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.content {
|
|
22
|
+
flex: 1;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#message {
|
|
29
|
+
color: var(--theme-color-primary, #972eff);
|
|
30
|
+
}
|
package/scripts/create.js
CHANGED
|
@@ -1,60 +1,9 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import { toTitleCase, walkTextFiles, replaceInFile } from './template-utils.js'
|
|
4
|
+
import { parseCreateArgs, scaffoldNewApp } from './create-scaffold.js'
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
'.ts',
|
|
6
|
-
'.tsx',
|
|
7
|
-
'.js',
|
|
8
|
-
'.jsx',
|
|
9
|
-
'.html',
|
|
10
|
-
'.css',
|
|
11
|
-
'.scss',
|
|
12
|
-
'.json',
|
|
13
|
-
'.yml',
|
|
14
|
-
'.yaml',
|
|
15
|
-
'.md',
|
|
16
|
-
'.txt',
|
|
17
|
-
'.svg',
|
|
18
|
-
'.gitignore',
|
|
19
|
-
'.ignore',
|
|
20
|
-
])
|
|
21
|
-
|
|
22
|
-
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git'])
|
|
23
|
-
|
|
24
|
-
function toTitleCase(kebab) {
|
|
25
|
-
return kebab
|
|
26
|
-
.split('-')
|
|
27
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
-
.join(' ')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function walkTextFiles(dir) {
|
|
32
|
-
const results = []
|
|
33
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
-
const fullPath = path.join(dir, entry.name)
|
|
35
|
-
if (entry.isDirectory()) {
|
|
36
|
-
if (!SKIP_DIRS.has(entry.name)) results.push(...walkTextFiles(fullPath))
|
|
37
|
-
} else if (
|
|
38
|
-
entry.isFile() &&
|
|
39
|
-
(TEXT_EXTENSIONS.has(path.extname(entry.name)) ||
|
|
40
|
-
TEXT_EXTENSIONS.has(entry.name))
|
|
41
|
-
) {
|
|
42
|
-
results.push(fullPath)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return results
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function replaceInFile(filePath, replacements) {
|
|
49
|
-
const original = fs.readFileSync(filePath, 'utf-8')
|
|
50
|
-
const updated = Object.entries(replacements).reduce(
|
|
51
|
-
(src, [placeholder, value]) => src.replaceAll(placeholder, value),
|
|
52
|
-
original,
|
|
53
|
-
)
|
|
54
|
-
if (updated !== original) fs.writeFileSync(filePath, updated, 'utf-8')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function createCommand(_args) {
|
|
6
|
+
function initializeExistingProject() {
|
|
58
7
|
const projectRoot = process.cwd()
|
|
59
8
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
60
9
|
|
|
@@ -64,7 +13,9 @@ export async function createCommand(_args) {
|
|
|
64
13
|
} catch (error) {
|
|
65
14
|
console.error(
|
|
66
15
|
`Failed to read or parse package.json at ${pkgPath}. ` +
|
|
67
|
-
'Make sure you are running this command from an Edge App project root
|
|
16
|
+
'Make sure you are running this command from an Edge App project root, ' +
|
|
17
|
+
'or pass a directory name to scaffold a new Edge App: ' +
|
|
18
|
+
'npx @screenly/edge-apps create <directory> (or bunx @screenly/edge-apps create <directory>)',
|
|
68
19
|
)
|
|
69
20
|
if (error instanceof Error && error.message) {
|
|
70
21
|
console.error(`Details: ${error.message}`)
|
|
@@ -93,7 +44,7 @@ export async function createCommand(_args) {
|
|
|
93
44
|
replaceInFile(filePath, replacements)
|
|
94
45
|
}
|
|
95
46
|
|
|
96
|
-
const updatedPkg =
|
|
47
|
+
const updatedPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
97
48
|
delete updatedPkg['bun-create']
|
|
98
49
|
fs.writeFileSync(pkgPath, JSON.stringify(updatedPkg, null, 2) + '\n', 'utf-8')
|
|
99
50
|
|
|
@@ -113,3 +64,38 @@ Next steps:
|
|
|
113
64
|
npm run deploy
|
|
114
65
|
`)
|
|
115
66
|
}
|
|
67
|
+
|
|
68
|
+
function hasScaffoldOnlyOptions(options) {
|
|
69
|
+
return (
|
|
70
|
+
options.description !== null ||
|
|
71
|
+
options.author !== null ||
|
|
72
|
+
options.pm !== null ||
|
|
73
|
+
options.force ||
|
|
74
|
+
options.skipInstall
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function createCommand(args) {
|
|
79
|
+
const { directory, options, error } = parseCreateArgs(args)
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
console.error(error)
|
|
83
|
+
process.exitCode = 1
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!directory && hasScaffoldOnlyOptions(options)) {
|
|
88
|
+
console.error(
|
|
89
|
+
'Options like --description/--author/--pm/--force/--skip-install only apply ' +
|
|
90
|
+
'when scaffolding a new app: npx @screenly/edge-apps create <directory>',
|
|
91
|
+
)
|
|
92
|
+
process.exitCode = 1
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (directory) {
|
|
97
|
+
scaffoldNewApp(directory, options)
|
|
98
|
+
} else {
|
|
99
|
+
initializeExistingProject()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
const TEXT_EXTENSIONS = new Set([
|
|
5
|
+
'.ts',
|
|
6
|
+
'.tsx',
|
|
7
|
+
'.js',
|
|
8
|
+
'.jsx',
|
|
9
|
+
'.html',
|
|
10
|
+
'.css',
|
|
11
|
+
'.scss',
|
|
12
|
+
'.json',
|
|
13
|
+
'.yml',
|
|
14
|
+
'.yaml',
|
|
15
|
+
'.md',
|
|
16
|
+
'.txt',
|
|
17
|
+
'.svg',
|
|
18
|
+
'.gitignore',
|
|
19
|
+
'.ignore',
|
|
20
|
+
'_gitignore',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git'])
|
|
24
|
+
|
|
25
|
+
export function toTitleCase(kebab) {
|
|
26
|
+
return kebab
|
|
27
|
+
.split('-')
|
|
28
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
29
|
+
.join(' ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toKebabCase(name) {
|
|
33
|
+
const kebab = name
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '')
|
|
38
|
+
return kebab || 'my-edge-app'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function walkTextFiles(dir) {
|
|
42
|
+
const results = []
|
|
43
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const fullPath = path.join(dir, entry.name)
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (!SKIP_DIRS.has(entry.name)) results.push(...walkTextFiles(fullPath))
|
|
47
|
+
} else if (
|
|
48
|
+
entry.isFile() &&
|
|
49
|
+
(TEXT_EXTENSIONS.has(path.extname(entry.name)) ||
|
|
50
|
+
TEXT_EXTENSIONS.has(entry.name))
|
|
51
|
+
) {
|
|
52
|
+
results.push(fullPath)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return results
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function replaceInFile(filePath, replacements) {
|
|
59
|
+
const original = fs.readFileSync(filePath, 'utf-8')
|
|
60
|
+
const updated = Object.entries(replacements).reduce(
|
|
61
|
+
(src, [placeholder, value]) => src.replaceAll(placeholder, value),
|
|
62
|
+
original,
|
|
63
|
+
)
|
|
64
|
+
if (updated !== original) fs.writeFileSync(filePath, updated, 'utf-8')
|
|
65
|
+
}
|