@screenly/edge-apps 1.1.1 → 1.2.0

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
@@ -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.
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@screenly/edge-apps",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "A TypeScript library for interfacing with Screenly Edge Apps API",
5
5
  "type": "module",
6
- "sideEffects": ["**/*.css", "**/register.js", "**/buffer-shim.js"],
6
+ "sideEffects": [
7
+ "**/*.css",
8
+ "**/register.js",
9
+ "**/buffer-shim.js",
10
+ "**/components/**"
11
+ ],
7
12
  "engines": {
8
13
  "node": ">=20.6.0"
9
14
  },
@@ -103,7 +108,7 @@
103
108
  "tailwindcss": "^4.2.1",
104
109
  "typescript": "^5.9.3",
105
110
  "typescript-eslint": "^8.57.0",
106
- "vite": "^7.3.1",
111
+ "vite": "^8.1.2",
107
112
  "yaml": "^2.8.2"
108
113
  },
109
114
  "peerDependencies": {
@@ -118,9 +123,14 @@
118
123
  "@types/jsdom": "^28.0.0",
119
124
  "@types/node": "^25.4.0",
120
125
  "@types/sharp": "^0.32.0",
121
- "@vitest/coverage-v8": "^3.2.4",
126
+ "@vitest/coverage-v8": "^4.1.9",
122
127
  "globals": "^13.24.0",
123
128
  "prettier": "^3.8.1",
124
- "vitest": "^3.2.4"
129
+ "vitest": "^4.1.9"
130
+ },
131
+ "overrides": {
132
+ "@jsheaven/easybuild": {
133
+ "esbuild": "0.25.12"
134
+ }
125
135
  }
126
136
  }
package/scripts/cli.js CHANGED
@@ -42,7 +42,7 @@ const commands = {
42
42
  },
43
43
  create: {
44
44
  description:
45
- 'Initialize a scaffolded Edge App (replaces template placeholders)',
45
+ 'Scaffold a new Edge App (or replace template placeholders in the current project)',
46
46
  handler: createCommand,
47
47
  },
48
48
  }
@@ -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,6 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": false,
4
+ "singleQuote": true,
5
+ "printWidth": 80
6
+ }
@@ -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,5 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .DS_Store
5
+ mock-data.yml
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@screenly/edge-apps/tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
8
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ },
7
+ })
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
- 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
- ])
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 = { ...pkg }
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
+ }