create-lingcode 0.1.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.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // npm create lingcode@latest [directory] [--template <name>] [--no-install]
3
+
4
+ import { spawnSync } from 'node:child_process';
5
+ import { existsSync, readdirSync } from 'node:fs';
6
+ import { cp, readFile, rename, writeFile } from 'node:fs/promises';
7
+ import { basename, join, relative, resolve } from 'node:path';
8
+ import { createInterface } from 'node:readline/promises';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const TEMPLATES_DIR = fileURLToPath(new URL('../templates/', import.meta.url));
12
+ const DEFAULT_TEMPLATE = 'react-vite-chat';
13
+
14
+ export function parseArgs(argv) {
15
+ const out = { dir: null, template: DEFAULT_TEMPLATE, install: true };
16
+ for (let i = 0; i < argv.length; i += 1) {
17
+ const arg = argv[i];
18
+ if (arg === '--template' || arg === '-t') out.template = argv[++i];
19
+ else if (arg.startsWith('--template=')) out.template = arg.slice('--template='.length);
20
+ else if (arg === '--no-install') out.install = false;
21
+ else if (!arg.startsWith('-') && !out.dir) out.dir = arg;
22
+ }
23
+ return out;
24
+ }
25
+
26
+ export function packageNameFrom(dir) {
27
+ const name = basename(resolve(dir)).toLowerCase().replace(/[^a-z0-9-~._]+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
28
+ return name || 'lingcode-app';
29
+ }
30
+
31
+ // The package manager that ran `create`, so installs match the user's choice.
32
+ export function packageManager(env = process.env) {
33
+ const agent = String(env.npm_config_user_agent || '');
34
+ for (const pm of ['pnpm', 'yarn', 'bun']) if (agent.startsWith(pm)) return pm;
35
+ return 'npm';
36
+ }
37
+
38
+ export async function scaffold({ dir, template = DEFAULT_TEMPLATE }) {
39
+ const source = join(TEMPLATES_DIR, template);
40
+ if (!existsSync(source)) {
41
+ const available = readdirSync(TEMPLATES_DIR).join(', ');
42
+ throw new Error(`Unknown template "${template}". Available: ${available}`);
43
+ }
44
+ const target = resolve(dir);
45
+ if (existsSync(target) && readdirSync(target).length) {
46
+ throw new Error(`${relative(process.cwd(), target) || target} is not empty.`);
47
+ }
48
+ await cp(source, target, { recursive: true });
49
+ // npm strips .gitignore from published packages, so templates ship _gitignore.
50
+ if (existsSync(join(target, '_gitignore'))) await rename(join(target, '_gitignore'), join(target, '.gitignore'));
51
+ const pkgFile = join(target, 'package.json');
52
+ const pkg = JSON.parse(await readFile(pkgFile, 'utf8'));
53
+ pkg.name = packageNameFrom(target);
54
+ await writeFile(pkgFile, `${JSON.stringify(pkg, null, 2)}\n`);
55
+ return target;
56
+ }
57
+
58
+ async function main(argv) {
59
+ const args = parseArgs(argv);
60
+ if (!args.dir) {
61
+ if (process.stdin.isTTY) {
62
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
63
+ args.dir = (await rl.question('Project directory (my-lingcode-app): ')).trim() || 'my-lingcode-app';
64
+ rl.close();
65
+ } else {
66
+ args.dir = 'my-lingcode-app';
67
+ }
68
+ }
69
+ const target = await scaffold(args);
70
+ const shown = relative(process.cwd(), target) || '.';
71
+ console.log(`Created ${shown} from the ${args.template} template.`);
72
+
73
+ const pm = packageManager();
74
+ if (args.install) {
75
+ console.log(`Installing dependencies with ${pm}…`);
76
+ const result = spawnSync(pm, ['install'], { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' });
77
+ if (result.status !== 0) console.log(`\nInstall failed; run \`${pm} install\` inside ${shown} yourself.`);
78
+ }
79
+
80
+ const run = pm === 'npm' ? 'npm run' : pm;
81
+ const steps = [
82
+ [`cd ${shown}`, ''],
83
+ ...(args.install ? [] : [[`${pm} install`, '']]),
84
+ ['npx lingcode dev', '# sign in, create your dev backend, deploy lingcode/'],
85
+ [`${run} dev`, '# in a second terminal: start the app'],
86
+ ];
87
+ const width = Math.max(...steps.filter(([, note]) => note).map(([cmd]) => cmd.length)) + 2;
88
+ console.log(`\nNext steps:\n${steps.map(([cmd, note]) => ` ${note ? cmd.padEnd(width) + note : cmd}`).join('\n')}\n`);
89
+ }
90
+
91
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
92
+ main(process.argv.slice(2)).catch((err) => {
93
+ console.error(`Error: ${err.message}`);
94
+ process.exitCode = 1;
95
+ });
96
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "create-lingcode",
3
+ "version": "0.1.0",
4
+ "description": "Create a new app on LingCode Cloud: `npm create lingcode@latest`.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-lingcode": "bin/create-lingcode.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test test/*.test.mjs"
18
+ },
19
+ "keywords": ["lingcode", "create", "template", "backend"],
20
+ "homepage": "https://lingcode.dev/docs/cloud/",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Xavierhuang/LingCode.git",
24
+ "directory": "packages/create-lingcode"
25
+ },
26
+ "license": "MIT",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ # LingCode chat
2
+
3
+ A realtime chat app on LingCode Cloud.
4
+
5
+ ```sh
6
+ npx lingcode dev # terminal 1: dev backend, .env.local, deploys lingcode/ on save
7
+ npm run dev # terminal 2: the Vite app
8
+ ```
9
+
10
+ Open the app in two tabs and send a message.
11
+
12
+ - `lingcode/migrations/` — SQL schema and row-level security. Add a new numbered file for every change.
13
+ - `lingcode/functions/` — server-side TypeScript functions, called with `lingcode.functions.invoke(name, body)`.
14
+ - `lingcode/_generated/` — types generated from your schema.
15
+
16
+ Ship the backend with `npx lingcode deploy`, which previews the production changes and asks before applying.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .env.local
@@ -0,0 +1,12 @@
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>LingCode chat</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,16 @@
1
+ -- Applied migrations are immutable. To change the schema, add 0002_….sql.
2
+ create table messages (
3
+ id uuid primary key default gen_random_uuid(),
4
+ author text not null check (char_length(author) between 1 and 40),
5
+ body text not null check (char_length(body) between 1 and 1000),
6
+ created_at timestamptz not null default now()
7
+ );
8
+
9
+ create index messages_created_at_idx on messages (created_at desc);
10
+
11
+ -- A public chat room: anyone with the app's anon key can read and post.
12
+ -- Before storing private data, add sign-in and scope policies to the user,
13
+ -- e.g. using (user_id::text = current_setting('app.user_id', true)).
14
+ alter table messages enable row level security;
15
+ create policy messages_read on messages for select using (true);
16
+ create policy messages_post on messages for insert with check (true);
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "lingcode-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "backend": "lingcode dev",
11
+ "deploy:backend": "lingcode deploy"
12
+ },
13
+ "dependencies": {
14
+ "lingcode-js": "^1.0.0",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-react": "^5.0.0",
20
+ "lingcode": "^0.1.0",
21
+ "vite": "^7.0.0"
22
+ }
23
+ }
@@ -0,0 +1,60 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { lingcode } from './lingcode.js';
3
+
4
+ function useMessages() {
5
+ const [messages, setMessages] = useState([]);
6
+ const [error, setError] = useState(null);
7
+
8
+ useEffect(() => {
9
+ let cancelled = false;
10
+ lingcode.from('messages').order('created_at', { ascending: false }).limit(100).select()
11
+ .then(({ data, error }) => {
12
+ if (cancelled) return;
13
+ if (error) setError(error.message);
14
+ else setMessages((data || []).reverse());
15
+ });
16
+ // Live updates from every open tab, filtered by row-level security.
17
+ const unsubscribe = lingcode.from('messages').subscribe(({ type, row }) => {
18
+ if (type !== 'INSERT') return;
19
+ setMessages((current) => (current.some((m) => m.id === row.id) ? current : [...current, row]));
20
+ });
21
+ return () => { cancelled = true; unsubscribe(); };
22
+ }, []);
23
+
24
+ return { messages, error };
25
+ }
26
+
27
+ export default function App() {
28
+ const { messages, error } = useMessages();
29
+ const [author, setAuthor] = useState(() => localStorage.getItem('author') || '');
30
+ const [body, setBody] = useState('');
31
+ const [sending, setSending] = useState(false);
32
+
33
+ async function send(event) {
34
+ event.preventDefault();
35
+ if (!author.trim() || !body.trim()) return;
36
+ setSending(true);
37
+ localStorage.setItem('author', author.trim());
38
+ const { error } = await lingcode.from('messages').insert({ author: author.trim(), body: body.trim() });
39
+ setSending(false);
40
+ if (error) alert(error.message);
41
+ else setBody('');
42
+ }
43
+
44
+ return (
45
+ <main>
46
+ <h1>Chat</h1>
47
+ {error && <p className="error">{error}</p>}
48
+ <ul className="messages">
49
+ {messages.map((m) => (
50
+ <li key={m.id}><strong>{m.author}</strong> {m.body}</li>
51
+ ))}
52
+ </ul>
53
+ <form onSubmit={send}>
54
+ <input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Name" maxLength={40} />
55
+ <input value={body} onChange={(e) => setBody(e.target.value)} placeholder="Message" maxLength={1000} autoFocus />
56
+ <button disabled={sending}>Send</button>
57
+ </form>
58
+ </main>
59
+ );
60
+ }
@@ -0,0 +1,10 @@
1
+ import { createClient } from 'lingcode-js';
2
+
3
+ const url = import.meta.env.VITE_LINGCODE_URL;
4
+ const anonKey = import.meta.env.VITE_LINGCODE_ANON_KEY;
5
+
6
+ if (!url || !anonKey) {
7
+ throw new Error('Missing VITE_LINGCODE_URL / VITE_LINGCODE_ANON_KEY. Run `npx lingcode dev` first; it writes .env.local.');
8
+ }
9
+
10
+ export const lingcode = createClient(url, anonKey);
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,8 @@
1
+ :root { font-family: system-ui, sans-serif; color-scheme: light dark; }
2
+ main { max-width: 640px; margin: 40px auto; padding: 0 16px; }
3
+ .messages { list-style: none; padding: 0; min-height: 200px; }
4
+ .messages li { padding: 6px 0; border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent); }
5
+ form { display: flex; gap: 8px; }
6
+ form input:first-child { width: 120px; }
7
+ form input:nth-child(2) { flex: 1; }
8
+ .error { color: #c0392b; }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });