create-sting-app 1.0.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 ADDED
@@ -0,0 +1,39 @@
1
+ # create-sting-app
2
+
3
+ Scaffold a StingJS starter app with JavaScript or TypeScript.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create sting@latest
9
+ ```
10
+
11
+ You can also run the package directly:
12
+
13
+ ```bash
14
+ npx create-sting-app@latest
15
+ ```
16
+
17
+ With a target directory:
18
+
19
+ ```bash
20
+ npm create sting@latest my-sting-app
21
+ ```
22
+
23
+ Select a template without prompts:
24
+
25
+ ```bash
26
+ npm create sting@latest my-sting-app -- --template typescript --yes
27
+ ```
28
+
29
+ Template values:
30
+ - `javascript` (`js`)
31
+ - `typescript` (`ts`)
32
+
33
+ ## Local Development
34
+
35
+ From repo root:
36
+
37
+ ```bash
38
+ node create-sting-app/bin/create-sting-app.js my-sting-app
39
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process"
3
+ import { run } from "../lib/run.js"
4
+
5
+ run(process.argv.slice(2), { cliName: "create-sting-app" }).catch((err) => {
6
+ console.error(`\n[create-sting-app] ${err?.message || err}`)
7
+ process.exit(1)
8
+ })
package/lib/run.js ADDED
@@ -0,0 +1,268 @@
1
+ import fs from "node:fs"
2
+ import fsp from "node:fs/promises"
3
+ import path from "node:path"
4
+ import process from "node:process"
5
+ import { fileURLToPath } from "node:url"
6
+ import { createInterface } from "node:readline/promises"
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ const templates = {
12
+ javascript: path.resolve(__dirname, "../template/vanilla-js"),
13
+ typescript: path.resolve(__dirname, "../template/vanilla-ts"),
14
+ }
15
+
16
+ const defaultProjectName = "sting-app"
17
+
18
+ export async function run(argv = process.argv.slice(2), options = {}) {
19
+ const cliName = options.cliName || "create-sting-app"
20
+ const parsed = parseArgs(argv)
21
+
22
+ if (parsed.help) {
23
+ printHelp(cliName)
24
+ return
25
+ }
26
+
27
+ const resolved = await resolveScaffoldOptions({
28
+ projectArg: parsed.projectArg,
29
+ templateArg: parsed.templateArg,
30
+ skipPrompts: parsed.skipPrompts,
31
+ })
32
+
33
+ const projectNameInput = resolved.projectName
34
+ const template = resolved.template
35
+ const templateRoot = templates[template]
36
+
37
+ if (!templateRoot) {
38
+ throw new Error(`unknown template: ${template}`)
39
+ }
40
+
41
+ const targetDir = path.resolve(process.cwd(), projectNameInput)
42
+ const inCurrentDir = projectNameInput === "."
43
+
44
+ const rawName = inCurrentDir ? path.basename(process.cwd()) : projectNameInput
45
+ const packageName = toPackageName(rawName)
46
+
47
+ await ensureTargetDir(targetDir, parsed.force)
48
+ await copyDir(templateRoot, targetDir)
49
+
50
+ const pkg = buildPackageJson({ packageName, template })
51
+ const pkgPath = path.join(targetDir, "package.json")
52
+ await fsp.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8")
53
+
54
+ console.log("\nDone. Created StingJS app:")
55
+ console.log(` ${targetDir}`)
56
+ console.log(`\nTemplate: ${template}`)
57
+ console.log("\nNext steps:")
58
+
59
+ if (!inCurrentDir) {
60
+ const relativeTarget = path.relative(process.cwd(), targetDir)
61
+ const cdTarget = !relativeTarget || relativeTarget.startsWith("..")
62
+ ? targetDir
63
+ : relativeTarget
64
+ console.log(` cd ${cdTarget}`)
65
+ }
66
+
67
+ console.log(" npm install")
68
+ console.log(" npm run dev")
69
+ console.log(`\nRun \`${cliName} --help\` for options.`)
70
+ }
71
+
72
+ function parseArgs(argv) {
73
+ let projectArg = ""
74
+ let templateArg = ""
75
+ let force = false
76
+ let skipPrompts = false
77
+ let help = false
78
+
79
+ for (let index = 0; index < argv.length; index += 1) {
80
+ const arg = argv[index]
81
+
82
+ if (arg === "--force" || arg === "-f") {
83
+ force = true
84
+ continue
85
+ }
86
+
87
+ if (arg === "--yes" || arg === "-y") {
88
+ skipPrompts = true
89
+ continue
90
+ }
91
+
92
+ if (arg === "--help" || arg === "-h") {
93
+ help = true
94
+ continue
95
+ }
96
+
97
+ if (arg === "--template" || arg === "-t") {
98
+ const nextArg = argv[index + 1]
99
+ if (!nextArg || nextArg.startsWith("-")) {
100
+ throw new Error("missing value for --template. Use --template javascript or --template typescript.")
101
+ }
102
+ templateArg = nextArg
103
+ index += 1
104
+ continue
105
+ }
106
+
107
+ if (arg.startsWith("--template=")) {
108
+ templateArg = arg.slice("--template=".length)
109
+ continue
110
+ }
111
+
112
+ if (!arg.startsWith("-") && !projectArg) {
113
+ projectArg = arg
114
+ }
115
+ }
116
+
117
+ return { projectArg, templateArg, force, skipPrompts, help }
118
+ }
119
+
120
+ function printHelp(cliName) {
121
+ console.log(cliName)
122
+ console.log("")
123
+ console.log("Usage:")
124
+ console.log(` ${cliName} [project-name] [options]`)
125
+ console.log("")
126
+ console.log("Options:")
127
+ console.log(" -t, --template <name> javascript | typescript")
128
+ console.log(" -f, --force allow scaffolding into a non-empty directory")
129
+ console.log(" -y, --yes skip prompts and use defaults")
130
+ console.log(" -h, --help show this message")
131
+ }
132
+
133
+ async function resolveScaffoldOptions({ projectArg, templateArg, skipPrompts }) {
134
+ const normalizedProject = projectArg && projectArg.trim() ? projectArg.trim() : ""
135
+ const normalizedTemplate = normalizeTemplate(templateArg)
136
+
137
+ if (skipPrompts) {
138
+ return {
139
+ projectName: normalizedProject || defaultProjectName,
140
+ template: normalizedTemplate || "javascript",
141
+ }
142
+ }
143
+
144
+ if (normalizedProject && normalizedTemplate) {
145
+ return {
146
+ projectName: normalizedProject,
147
+ template: normalizedTemplate,
148
+ }
149
+ }
150
+
151
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
152
+
153
+ try {
154
+ const projectName = normalizedProject || await promptProjectName(rl)
155
+ const template = normalizedTemplate || await promptTemplate(rl)
156
+ return { projectName, template }
157
+ } finally {
158
+ rl.close()
159
+ }
160
+ }
161
+
162
+ async function promptProjectName(rl) {
163
+ const answer = (await rl.question(`Project name (${defaultProjectName}): `)).trim()
164
+ return answer || defaultProjectName
165
+ }
166
+
167
+ async function promptTemplate(rl) {
168
+ for (;;) {
169
+ const answer = (await rl.question("Language? [1] JavaScript [2] TypeScript (1): ")).trim().toLowerCase()
170
+
171
+ if (!answer || answer === "1" || answer === "javascript" || answer === "js") {
172
+ return "javascript"
173
+ }
174
+
175
+ if (answer === "2" || answer === "typescript" || answer === "ts") {
176
+ return "typescript"
177
+ }
178
+
179
+ console.log("Please enter 1 (JavaScript) or 2 (TypeScript).")
180
+ }
181
+ }
182
+
183
+ function normalizeTemplate(input) {
184
+ if (typeof input !== "string") return ""
185
+
186
+ const normalized = input.trim().toLowerCase()
187
+ if (!normalized) return ""
188
+
189
+ if (normalized === "javascript" || normalized === "js" || normalized === "vanilla" || normalized === "vanilla-js") {
190
+ return "javascript"
191
+ }
192
+
193
+ if (normalized === "typescript" || normalized === "ts" || normalized === "vanilla-ts") {
194
+ return "typescript"
195
+ }
196
+
197
+ throw new Error(`unknown template \"${input}\". Use \"javascript\" or \"typescript\".`)
198
+ }
199
+
200
+ function buildPackageJson({ packageName, template }) {
201
+ const base = {
202
+ name: packageName,
203
+ private: true,
204
+ version: "0.0.0",
205
+ type: "module",
206
+ scripts: {
207
+ dev: "vite",
208
+ build: "vite build",
209
+ preview: "vite preview",
210
+ },
211
+ dependencies: {
212
+ stingjs: "^1.0.0",
213
+ },
214
+ devDependencies: {
215
+ vite: "^7.3.1",
216
+ },
217
+ }
218
+
219
+ if (template === "typescript") {
220
+ base.scripts.typecheck = "tsc --noEmit"
221
+ base.devDependencies.typescript = "^5.9.3"
222
+ }
223
+
224
+ return base
225
+ }
226
+
227
+ async function ensureTargetDir(targetDir, force) {
228
+ if (!fs.existsSync(targetDir)) {
229
+ await fsp.mkdir(targetDir, { recursive: true })
230
+ return
231
+ }
232
+
233
+ const entries = await fsp.readdir(targetDir)
234
+ if (entries.length === 0) return
235
+
236
+ if (!force) {
237
+ throw new Error(`target directory is not empty: ${targetDir}. Use --force to continue.`)
238
+ }
239
+ }
240
+
241
+ async function copyDir(srcDir, destDir) {
242
+ const entries = await fsp.readdir(srcDir, { withFileTypes: true })
243
+
244
+ for (const entry of entries) {
245
+ const srcPath = path.join(srcDir, entry.name)
246
+ const name = entry.name === "_gitignore" ? ".gitignore" : entry.name
247
+ const destPath = path.join(destDir, name)
248
+
249
+ if (entry.isDirectory()) {
250
+ await fsp.mkdir(destPath, { recursive: true })
251
+ await copyDir(srcPath, destPath)
252
+ continue
253
+ }
254
+
255
+ await fsp.copyFile(srcPath, destPath)
256
+ }
257
+ }
258
+
259
+ function toPackageName(input) {
260
+ const trimmed = input.trim().toLowerCase()
261
+ const sanitized = trimmed
262
+ .replace(/^\.+/, "")
263
+ .replace(/[^a-z0-9._-]+/g, "-")
264
+ .replace(/^-+/, "")
265
+ .replace(/-+$/, "")
266
+
267
+ return sanitized || defaultProjectName
268
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "create-sting-app",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a StingJS app",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-sting-app": "./bin/create-sting-app.js"
8
+ },
9
+ "exports": {
10
+ "./cli": "./lib/run.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "lib",
15
+ "template",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "stingjs",
20
+ "create-sting",
21
+ "create-sting-app",
22
+ "scaffold",
23
+ "vite",
24
+ "typescript"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ }
30
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ *.log
@@ -0,0 +1,22 @@
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>Sting App</title>
7
+ </head>
8
+ <body>
9
+ <main class="app" x-data="counter">
10
+ <h1>StingJS Starter</h1>
11
+ <p class="hint">Counter demo powered by StingJS directives.</p>
12
+
13
+ <div class="row">
14
+ <button x-on:click="dec">Decrease</button>
15
+ <span class="count" x-text="count"></span>
16
+ <button x-on:click="inc">Increase</button>
17
+ </div>
18
+ </main>
19
+
20
+ <script type="module" src="/src/main.js"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,15 @@
1
+ import sting from "stingjs"
2
+ import "./style.css"
3
+
4
+ sting.data("counter", () => {
5
+ const [count, setCount] = sting.signal(0)
6
+
7
+ const inc = () => setCount((value) => value + 1)
8
+ const dec = () => setCount((value) => value - 1)
9
+
10
+ return {
11
+ count,
12
+ inc,
13
+ dec,
14
+ }
15
+ })
@@ -0,0 +1,63 @@
1
+ :root {
2
+ font-family: "Avenir Next", "Segoe UI", sans-serif;
3
+ line-height: 1.4;
4
+ color: #0f172a;
5
+ background: radial-gradient(circle at top, #dbeafe 0%, #eff6ff 45%, #ffffff 100%);
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ min-height: 100vh;
15
+ display: grid;
16
+ place-items: center;
17
+ }
18
+
19
+ .app {
20
+ width: min(560px, calc(100vw - 32px));
21
+ background: #ffffff;
22
+ border: 1px solid #bfdbfe;
23
+ border-radius: 16px;
24
+ padding: 24px;
25
+ box-shadow: 0 16px 36px rgba(30, 64, 175, 0.16);
26
+ }
27
+
28
+ h1 {
29
+ margin: 0 0 8px;
30
+ }
31
+
32
+ .hint {
33
+ margin: 0 0 20px;
34
+ color: #334155;
35
+ }
36
+
37
+ .row {
38
+ display: flex;
39
+ gap: 12px;
40
+ align-items: center;
41
+ }
42
+
43
+ button {
44
+ border: 1px solid #93c5fd;
45
+ background: #eff6ff;
46
+ color: #1d4ed8;
47
+ border-radius: 10px;
48
+ padding: 8px 14px;
49
+ font-size: 0.95rem;
50
+ font-weight: 600;
51
+ cursor: pointer;
52
+ }
53
+
54
+ button:hover {
55
+ background: #dbeafe;
56
+ }
57
+
58
+ .count {
59
+ min-width: 52px;
60
+ text-align: center;
61
+ font-size: 1.5rem;
62
+ font-weight: 700;
63
+ }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ *.log
@@ -0,0 +1,22 @@
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>Sting App</title>
7
+ </head>
8
+ <body>
9
+ <main class="app" x-data="counter">
10
+ <h1>StingJS Starter</h1>
11
+ <p class="hint">Counter demo powered by StingJS directives.</p>
12
+
13
+ <div class="row">
14
+ <button x-on:click="dec">Decrease</button>
15
+ <span class="count" x-text="count"></span>
16
+ <button x-on:click="inc">Increase</button>
17
+ </div>
18
+ </main>
19
+
20
+ <script type="module" src="/src/main.ts"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,15 @@
1
+ import sting from "stingjs"
2
+ import "./style.css"
3
+
4
+ sting.data("counter", () => {
5
+ const [count, setCount] = sting.signal(0)
6
+
7
+ const inc = () => setCount((value) => value + 1)
8
+ const dec = () => setCount((value) => value - 1)
9
+
10
+ return {
11
+ count,
12
+ inc,
13
+ dec,
14
+ }
15
+ })
@@ -0,0 +1,9 @@
1
+ declare module "stingjs" {
2
+ interface StingApi {
3
+ data(name: string, setup: () => Record<string, unknown>): void
4
+ signal<T>(initialValue: T): [() => T, (nextValue: T | ((prev: T) => T)) => void]
5
+ }
6
+
7
+ const sting: StingApi
8
+ export default sting
9
+ }
@@ -0,0 +1,63 @@
1
+ :root {
2
+ font-family: "Avenir Next", "Segoe UI", sans-serif;
3
+ line-height: 1.4;
4
+ color: #0f172a;
5
+ background: radial-gradient(circle at top, #dbeafe 0%, #eff6ff 45%, #ffffff 100%);
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ min-height: 100vh;
15
+ display: grid;
16
+ place-items: center;
17
+ }
18
+
19
+ .app {
20
+ width: min(560px, calc(100vw - 32px));
21
+ background: #ffffff;
22
+ border: 1px solid #bfdbfe;
23
+ border-radius: 16px;
24
+ padding: 24px;
25
+ box-shadow: 0 16px 36px rgba(30, 64, 175, 0.16);
26
+ }
27
+
28
+ h1 {
29
+ margin: 0 0 8px;
30
+ }
31
+
32
+ .hint {
33
+ margin: 0 0 20px;
34
+ color: #334155;
35
+ }
36
+
37
+ .row {
38
+ display: flex;
39
+ gap: 12px;
40
+ align-items: center;
41
+ }
42
+
43
+ button {
44
+ border: 1px solid #93c5fd;
45
+ background: #eff6ff;
46
+ color: #1d4ed8;
47
+ border-radius: 10px;
48
+ padding: 8px 14px;
49
+ font-size: 0.95rem;
50
+ font-weight: 600;
51
+ cursor: pointer;
52
+ }
53
+
54
+ button:hover {
55
+ background: #dbeafe;
56
+ }
57
+
58
+ .count {
59
+ min-width: 52px;
60
+ text-align: center;
61
+ font-size: 1.5rem;
62
+ font-weight: 700;
63
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "types": ["vite/client"]
10
+ },
11
+ "include": ["src"]
12
+ }