Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,49 @@ Or with Bun:
bun add @screenly/edge-apps
```

## Creating a New Edge App

Scaffold a new Edge App without installing anything first:

```bash
npx @screenly/edge-apps create my-edge-app
```

Or with Bun:

```bash
bunx @screenly/edge-apps create my-edge-app
```

The package manager used to invoke the command is detected automatically, so
the generated `package.json` scripts are wired up to match (`npm run ...` vs.
`bun run ...`). This generates a minimal Edge App with a `screenly.yml`
manifest, `index.html`, and a `src/main.ts` entry point wired up to this
library.

Pass options after the directory name, for example:

```bash
npx @screenly/edge-apps create my-edge-app --description "My Edge App" --author "Jane Doe"
```

| Option | Description |
| ---------------------- | ------------------------------------------------------------------- |
| `--description <text>` | Description used in `package.json`, `screenly.yml`, and `README.md` |
| `--author <text>` | Author name added to `package.json` |
| `--pm <npm\|bun>` | Force a package manager instead of auto-detecting it |
| `--force` | Write into an existing, non-empty directory |
| `--skip-install` | Skip installing dependencies after scaffolding |

> [!NOTE]
> Running `edge-apps-scripts create` with no directory argument instead
> replaces `{{APP_NAME}}`-style placeholders in the current project — this is
> used by Edge App template repositories after cloning, and is unrelated to
> scaffolding a brand new app.

No test files are included — `test`/`test:unit` work out of the box on an
empty suite; add your own under `src/` when you have something to test.

### Local Development Setup

When developing Edge Apps locally using the library from this repository, you should link the package.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@screenly/edge-apps",
"version": "1.1.2",
"version": "1.2.0",
"description": "A TypeScript library for interfacing with Screenly Edge Apps API",
"type": "module",
"sideEffects": [
Expand Down
2 changes: 1 addition & 1 deletion scripts/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const commands = {
},
create: {
description:
'Initialize a scaffolded Edge App (replaces template placeholders)',
'Scaffold a new Edge App (or replace template placeholders in the current project)',
handler: createCommand,
},
}
Expand Down
234 changes: 234 additions & 0 deletions scripts/create-scaffold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import fs from 'fs'
import path from 'path'
import { spawnSync } from 'child_process'
import { fileURLToPath } from 'url'
import {
toKebabCase,
toTitleCase,
walkTextFiles,
replaceInFile,
} from './template-utils.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const libraryRoot = path.resolve(__dirname, '..')
const templateRoot = path.resolve(__dirname, 'create-template')
const libraryPkg = JSON.parse(
fs.readFileSync(path.join(libraryRoot, 'package.json'), 'utf-8'),
)

const NPM_RUN_ALL2_VERSION = '^8.0.4'
const TYPES_BUN_VERSION = '^1.3.13'
const BUN_TYPES_VERSION = '^1.3.13'

const VALUE_FLAGS = new Set(['--pm', '--description', '--author'])
const BOOLEAN_FLAGS = new Set(['--force', '--skip-install'])

export function parseCreateArgs(args) {
const options = {
pm: null,
description: null,
author: null,
force: false,
skipInstall: false,
}
const positional = []

for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (VALUE_FLAGS.has(arg)) {
Comment thread
nicomiguelino marked this conversation as resolved.
const value = args[i + 1]
if (value === undefined || value.startsWith('-')) {
return { error: `Missing value for ${arg}` }
}
i++
if (arg === '--pm') options.pm = value
else if (arg === '--description') options.description = value
else options.author = value
} else if (BOOLEAN_FLAGS.has(arg)) {
if (arg === '--force') options.force = true
else options.skipInstall = true
} else if (arg.startsWith('-')) {
return { error: `Unknown option: ${arg}` }
} else {
positional.push(arg)
}
}

if (positional.length > 1) {
return {
error: `Expected a single directory argument, got: ${positional.join(', ')}`,
}
}

return { directory: positional[0], options }
}

function validateDescription(description) {
if (description !== null && /[\r\n]/.test(description)) {
return 'Description cannot contain line breaks.'
}
return null
}

function detectPackageManager(explicit) {
if (explicit) {
if (explicit !== 'npm' && explicit !== 'bun') {
console.error(
`Unsupported package manager: ${explicit}. Use "npm" or "bun".`,
)
return null
}
return explicit
}

const userAgent = process.env.npm_config_user_agent || ''
return userAgent.startsWith('bun') ? 'bun' : 'npm'
}

function finalizePackageJson(destination, pm) {
const pkgPath = path.join(destination, 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))

pkg.devDependencies = {
...pkg.devDependencies,
'@screenly/edge-apps': `^${libraryPkg.version}`,
typescript: libraryPkg.dependencies.typescript,
prettier: libraryPkg.devDependencies.prettier,
'@types/node': libraryPkg.devDependencies['@types/node'],
'npm-run-all2': NPM_RUN_ALL2_VERSION,
'@playwright/test': libraryPkg.peerDependencies['@playwright/test'],
}

if (pm === 'bun') {
pkg.scripts.test = 'bun test --pass-with-no-tests src/'
pkg.devDependencies['@types/bun'] = TYPES_BUN_VERSION
pkg.devDependencies['bun-types'] = BUN_TYPES_VERSION
pkg.devDependencies.jsdom = libraryPkg.dependencies.jsdom
pkg.devDependencies['@types/jsdom'] =
libraryPkg.devDependencies['@types/jsdom']
} else {
pkg.scripts.test = 'vitest run --passWithNoTests'
pkg.devDependencies.vitest = libraryPkg.devDependencies.vitest
pkg.devDependencies.jsdom = libraryPkg.dependencies.jsdom
}
pkg.scripts['test:unit'] = pkg.scripts.test

fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
}

function installDependencies(destination, pm) {
console.log(`\nInstalling dependencies with ${pm}...`)
const result = spawnSync(pm, ['install'], {
cwd: destination,
stdio: 'inherit',
shell: process.platform === 'win32',
})
if (result.status !== 0) {
console.warn(
`\n${pm} install failed. Run it manually inside ${destination}.`,
)
}
}

function printScaffoldNextSteps(destination, appName, pm) {
const relativePath = path.relative(process.cwd(), destination) || '.'
const runCommand = pm === 'bun' ? 'bun run' : 'npm run'
const installCommand = pm === 'bun' ? 'bun install' : 'npm install'
const needsInstall = !fs.existsSync(path.join(destination, 'node_modules'))

const steps = [`cd "${relativePath}"`]
if (needsInstall) steps.push(installCommand)
steps.push(
`Register the app to get an id for screenly.yml:\n screenly edge-app create --name ${appName} --in-place`,
`Start the dev server:\n ${runCommand} dev`,
`Deploy when ready:\n ${runCommand} deploy`,
)

console.log(`
Done! Your Edge App is ready.

Next steps:
${steps.map((step, index) => ` ${index + 1}. ${step}`).join('\n')}
`)
}

export function scaffoldNewApp(directory, options) {
const pm = detectPackageManager(options.pm)
if (!pm) {
process.exitCode = 1
return
}

const descriptionError = validateDescription(options.description)
if (descriptionError) {
console.error(descriptionError)
process.exitCode = 1
return
}

const destination = path.resolve(process.cwd(), directory)

if (fs.existsSync(destination)) {
if (!fs.statSync(destination).isDirectory()) {
console.error(`"${directory}" already exists and is not a directory.`)
process.exitCode = 1
return
}
const isEmpty = fs.readdirSync(destination).length === 0
if (!isEmpty && !options.force) {
console.error(
`Directory "${directory}" already exists and is not empty. Use --force to write into it anyway.`,
)
process.exitCode = 1
return
}
} else {
fs.mkdirSync(destination, { recursive: true })
}

const appName = toKebabCase(path.basename(destination))
const appTitle = toTitleCase(appName)
const appDescription =
options.description ?? `${appTitle} - Screenly Edge App`

console.log(`\nScaffolding a new Edge App in ${destination}`)

fs.cpSync(templateRoot, destination, { recursive: true })
fs.copyFileSync(
path.join(destination, '_gitignore'),
path.join(destination, '.gitignore'),
)
fs.rmSync(path.join(destination, '_gitignore'))

const replacements = {
'{{APP_NAME}}': appName,
'{{APP_TITLE}}': appTitle,
'{{APP_DESCRIPTION}}': appDescription,
'{{APP_DESCRIPTION_JSON}}': JSON.stringify(appDescription).slice(1, -1),
'{{APP_DESCRIPTION_YAML}}': appDescription.replace(/'/g, "''"),
'{{PM_RUN}}': pm === 'bun' ? 'bun run' : 'npm run',
'{{PM_INSTALL}}': pm === 'bun' ? 'bun install' : 'npm install',
}
for (const filePath of walkTextFiles(destination)) {
replaceInFile(filePath, replacements)
}

finalizePackageJson(destination, pm)

if (options.author) {
const pkgPath = path.join(destination, 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.author = options.author
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8')
}

if (pm === 'bun') {
fs.rmSync(path.join(destination, 'vitest.config.ts'), { force: true })
}

if (!options.skipInstall) {
installDependencies(destination, pm)
}

printScaffoldNextSteps(destination, appName, pm)
}
6 changes: 6 additions & 0 deletions scripts/create-template/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 80
}
43 changes: 43 additions & 0 deletions scripts/create-template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# {{APP_TITLE}}

{{APP_DESCRIPTION}}

## Getting Started

Install dependencies:

```bash
{{PM_INSTALL}}
```

## Development

```bash
{{PM_RUN}} dev
```

## Build

```bash
{{PM_RUN}} build
```

## Deployment

```bash
screenly edge-app create --name {{APP_NAME}} --in-place
{{PM_RUN}} deploy
screenly edge-app instance create
```

## Configuration

| Setting | Description | Required | Default |
| --------- | ------------------------------- | -------- | ------------------ |
| `message` | The message displayed on screen | No | `Hello, Screenly!` |

## Screenshots

```bash
{{PM_RUN}} screenshots
```
5 changes: 5 additions & 0 deletions scripts/create-template/_gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.DS_Store
mock-data.yml
Loading