Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
13 changes: 13 additions & 0 deletions js-pkg/create-y-sweet-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Create Y-Sweet App

Get started with [Y-Sweet](https://jamsocket.com/y-sweet/) by using `create-y-sweet-app`. This tool allows you to create a new project in your choice of framework, with Y-Sweet already set up — fully ready to build a collaborative multiplayer app!

Create a new project by running:

```sh
npx create-y-sweet-app@latest
```

You'll be asked for the name of your project and then prompted to choose a framework.

While Y-Sweet works with any web stack, `create-y-sweet-app` includes quick-start templates for [Next.js](https://nextjs.org) and [Remix](https://remix.run).
3 changes: 1 addition & 2 deletions js-pkg/create-y-sweet-app/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"name": "create-y-sweet-app",
"version": "1.0.0",
"main": "src/run.js",
"type": "module",
"bin": {
"create-y-sweet-app": "./src/run.js"
"create-y-sweet-app": "src/run.js"
},
"repository": {
"type": "git",
Expand Down
98 changes: 98 additions & 0 deletions js-pkg/create-y-sweet-app/src/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import readline from 'node:readline'

const SPINNER_FRAMES = [
'☀️',
'☀️',
Expand Down Expand Up @@ -60,3 +62,99 @@ export function gray(text) {
export function bold(text) {
return `\x1b[1m${text}\x1b[0m`
}

export function question(prompt, defaultValue = '') {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

rl.question(prompt, (result) => {
rl.close()
resolve(result || defaultValue)
})
})
Comment thread
jakelazaroff marked this conversation as resolved.
}

/**
* Creates an interactive multiple choice selection menu without clearing console
* @param {string} prompt - The question to display above choices
* @param {string[]} choices - Array of options user can choose from
* @returns {Promise<string>} - Returns selected choice
*/
export function select(prompt, choices) {
return new Promise((resolve) => {
let selected = 0
let lines = 0 // track number of lines we've rendered

function render() {
// move cursor up to clear previous render
if (lines > 0) process.stdout.write(`\x1B[${lines}A`)

// render prompt and choices
console.log(bold(prompt) + '\n')
choices.forEach((choice, i) => {
const indicator = i === selected ? '❯' : ' '
console.log(`${indicator} ${choice}`)
})

// Ssore number of lines we just rendered
lines = choices.length + 2 // +2 for prompt and blank line
}

function cleanup() {
// remove event listener
process.stdin.off('data', onData)

// clean up the menu
process.stdout.write(`\x1B[${lines}A`) // Move up
process.stdout.write(`\x1B[J`) // Clear to bottom

// restore cursor and normal mode
process.stdout.write('\x1B[?25h')
process.stdin.setRawMode(false)
process.stdin.pause()
}

/** @param {Buffer} data */
function onData(data) {
const key = data.toString()

switch (key) {
case '\u001b[A': {
selected = (selected + choices.length - 1) % choices.length
render()
break
}

case '\u001b[B': {
selected = (selected + 1) % choices.length
render()
break
}

case '\r': {
cleanup()
console.log(bold(prompt) + ' ' + choices[selected])
resolve(choices[selected])
break
}

case '\u0003': {
cleanup()
return process.exit(1)
}
}
}

// hide cursor and enable raw mode
process.stdout.write('\x1B[?25l')
process.stdin.setRawMode(true)
process.stdin.resume()

render()

process.stdin.on('data', onData)
})
Comment thread
jakelazaroff marked this conversation as resolved.
}
21 changes: 21 additions & 0 deletions js-pkg/create-y-sweet-app/src/frameworks/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
This is a [Y-Sweet](https://jamsocket.com/y-sweet) project using [Next.js](https://nextjs.org). It was bootstrapped with [`create-y-sweet-app`](https://www.npmjs.com/package/create-y-sweet-app).

## Getting Started

First, run the development server:

```sh
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser to see the app running. If you open it in multiple windows, you'll see that all the state in the app is automatically kept in sync!

You can start editing the page by modifying `app/page.tsx`.

## Learn More

While you're building your Y-Sweet app, you might also want to check out:

- [Y-Sweet documentation](https://docs.jamsocket.com/y-sweet/) - learn about Y-Sweet's features and API.
- [Discord server](https://discord.gg/N5sEpsuhh9) - chat with the developers and get help from other users.
- [Y-Sweet GitHub repository](https://github.com/jamsocket/y-sweet) - leave feedback, report bugs and contribute to Y-Sweet.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DocumentManager } from "@y-sweet/sdk";
import { YDocProvider } from "@y-sweet/react";

import { MyCollaborativeApp } from "@/components/MyCollaborativeApp";
import { App } from "@/components/App";

const manager = new DocumentManager(process.env.CONNECTION_STRING!);

Expand All @@ -17,7 +17,7 @@ export default async function Home() {

return (
<YDocProvider docId={docId} authEndpoint={getClientToken}>
<MyCollaborativeApp />
<App />
</YDocProvider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Presence } from "./Presence";
import { Footer } from "./Footer";
import { Todos } from "./Todos";

export function MyCollaborativeApp() {
export function App() {
return (
<div className="min-h-screen flex flex-col justify-between max-w-[60rem] mx-auto before:content-[''] before:block before:absolute before:top-0 before:left-0 before:right-0 before:border-t-4 before:border-[#fc5c86]">
<header className="flex flex-col gap-8 p-12">
Expand Down
21 changes: 21 additions & 0 deletions js-pkg/create-y-sweet-app/src/frameworks/remix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
This is a [Y-Sweet](https://jamsocket.com/y-sweet) project using [Remix](https://remix.run). It was bootstrapped with [`create-y-sweet-app`](https://www.npmjs.com/package/create-y-sweet-app).

## Getting Started

First, run the development server:

```sh
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser to see the app running. If you open it in multiple windows, you'll see that all the state in the app is automatically kept in sync!

You can start editing the page by modifying `app/page.tsx`.

## Learn More

While you're building your Y-Sweet app, you might also want to check out:

- [Y-Sweet documentation](https://docs.jamsocket.com/y-sweet/) - learn about Y-Sweet's features and API.
- [Discord server](https://discord.gg/N5sEpsuhh9) - chat with the developers and get help from other users.
- [Y-Sweet GitHub repository](https://github.com/jamsocket/y-sweet) - leave feedback, report bugs and contribute to Y-Sweet.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Presence } from "./Presence";
import { Footer } from "./Footer";
import { Todos } from "./Todos";

export function MyCollaborativeApp() {
export function App() {
return (
<div className="min-h-screen flex flex-col justify-between max-w-[60rem] mx-auto before:content-[''] before:block before:absolute before:top-0 before:left-0 before:right-0 before:border-t-4 before:border-[#fc5c86]">
<header className="flex flex-col gap-8 p-12">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type MetaFunction } from "@remix-run/react";
import { YDocProvider } from "@y-sweet/react";

import { MyCollaborativeApp } from "~/components/MyCollaborativeApp";
import { App } from "~/components/App";

export const meta: MetaFunction = () => [{ title: "Y-Sweet + Remix" }];

Expand All @@ -18,7 +18,7 @@ export default function Home() {

return (
<YDocProvider docId={docId} authEndpoint="/auth">
<MyCollaborativeApp />
<App />
</YDocProvider>
);
}
64 changes: 30 additions & 34 deletions js-pkg/create-y-sweet-app/src/run.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,66 +1,62 @@
#!/usr/bin/env node

import { cp, readdir } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import readline from 'node:readline'

import { spinner, bold, gray } from './cli.js'
import { select, question, spinner, bold, gray } from './cli.js'
import { execute } from './shell.js'

const TEMPLATES = new Set(['nextjs', 'remix'])

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

/**
* @param {string} prompt
* @returns {Promise<string>}
*/
function question(prompt, defaultValue = '') {
return new Promise((resolve) => rl.question(prompt, (result) => resolve(result || defaultValue)))
}
const FRAMEWORKS = new Set(['nextjs', 'remix'])

try {
const { values, positionals } = parseArgs({
options: { template: { type: 'string', short: 't' } },
options: { framework: { type: 'string', short: 'f' }, help: { type: 'boolean', short: 'h' } },
allowPositionals: true,
})

if (values.template) init(values.template, positionals[0])
else help()
} catch {
if (values.help) help()
else init({ framework: values.framework, name: positionals[0] })
} catch (e) {
console.log(e)
help()
}

function help() {
console.log(bold('Usage: npm create y-sweet-app --template <template> [name]'))
console.log('Available templates:', [...TEMPLATES].join(', '))
console.log(bold('Usage: create-y-sweet-app [name] [options]'))
console.log('\nOptions:')
console.log(' -f, --framework <framework>\t\tUse a specific framework')
console.log('\n Available frameworks:', [...FRAMEWORKS].join(', '), '\n')
console.log(' -h, --help \t\tShow help')
process.exit(0)
}

/**
* @param {string} template
* @param {string} project
* @param {object} [options]
* @param {string} [options.framework]
* @param {string} [options.name]
*/
async function init(template, project) {
if (!TEMPLATES.has(template)) {
console.log(`No matching template for ${template}.`)
console.log(bold('Usage: npm create y-sweet-app --template <template> [name]'))
console.log('Available templates:', [...TEMPLATES].join(', '))
process.exit(1)
}
async function init(options = {}) {
let { framework, name } = options

let name = project
if (!name)
if (!name) {
name = await question(
bold('What do you want to call your app? ') + gray('(my-y-sweet-app) '),
'my-y-sweet-app',
)
}

if (!framework) framework = await select('What framework do you want to use?', [...FRAMEWORKS])
else if (!FRAMEWORKS.has(framework)) {
console.log(`No matching framework "${framework}".`)
console.log('Available frameworks:', [...FRAMEWORKS].join(', '))
process.exit(1)
}

const __dirname = dirname(fileURLToPath(import.meta.url))

const src = resolve(__dirname, './templates', template)
const src = resolve(__dirname, './frameworks', framework)
const dest = resolve(process.cwd(), name)

// ensure the destination directory is empty
Expand Down