create-flamefront 0.0.0 → 0.1.4

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,15 @@
1
+ # create-flamefront
2
+
3
+ Create a Flamefront app with one server-rendered Octane route:
4
+
5
+ ```sh
6
+ pnpm create flamefront@latest my-app
7
+ cd my-app
8
+ pnpm dev
9
+ ```
10
+
11
+ Use `--no-install` to create the project files without installing dependencies.
12
+ The target directory must be empty.
13
+
14
+ See the [Flamefront create-app guide](https://github.com/aleclarson/flamefront/blob/main/docs/create-app.md)
15
+ for the generated files and how they work.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../src/cli.js"
4
+
5
+ await run(process.argv.slice(2))
package/package.json CHANGED
@@ -1,13 +1,27 @@
1
1
  {
2
2
  "name": "create-flamefront",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "0.1.4",
4
+ "private": false,
5
+ "description": "Create a Flamefront app.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/aleclarson/flamefront.git",
10
+ "directory": "packages/create-flamefront"
8
11
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "All rights reserved",
12
- "type": "commonjs"
12
+ "type": "module",
13
+ "bin": {
14
+ "create-flamefront": "./bin/create-flamefront.js"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "src",
19
+ "template"
20
+ ],
21
+ "engines": {
22
+ "node": ">=26.0.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
13
27
  }
package/src/cli.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export type PackageManager = "bun" | "npm" | "pnpm" | "yarn"
2
+
3
+ export interface CreateProjectOptions {
4
+ readonly cwd?: string
5
+ readonly install?: boolean
6
+ readonly installDependencies?: (
7
+ packageManager: PackageManager,
8
+ directory: string,
9
+ ) => Promise<void> | void
10
+ readonly packageManager?: PackageManager
11
+ }
12
+
13
+ export interface CreatedProject {
14
+ readonly name: string
15
+ readonly packageManager: PackageManager
16
+ readonly targetDirectory: string
17
+ }
18
+
19
+ export function detectPackageManager(
20
+ environment?: Record<string, string | undefined>,
21
+ ): PackageManager
22
+
23
+ export function projectName(directory: string): string
24
+
25
+ export function createProject(
26
+ directory: string,
27
+ options?: CreateProjectOptions,
28
+ ): Promise<CreatedProject>
29
+
30
+ export function run(
31
+ arguments_: readonly string[],
32
+ options?: Omit<CreateProjectOptions, "install">,
33
+ ): Promise<void>
package/src/cli.js ADDED
@@ -0,0 +1,198 @@
1
+ import { spawn } from "node:child_process"
2
+ import {
3
+ cp,
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ rename,
8
+ stat,
9
+ writeFile,
10
+ } from "node:fs/promises"
11
+ import { basename, resolve } from "node:path"
12
+ import { fileURLToPath } from "node:url"
13
+
14
+ const templateDirectory = fileURLToPath(
15
+ new URL("../template/", import.meta.url),
16
+ )
17
+ const packageMetadata = JSON.parse(
18
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
19
+ )
20
+
21
+ const usage = `Create a Flamefront app.
22
+
23
+ Usage:
24
+ create-flamefront <directory> [--no-install]
25
+
26
+ Options:
27
+ --no-install Create the project without installing dependencies
28
+ -h, --help Show this help
29
+ -v, --version Show the installed create-flamefront version`
30
+
31
+ export function detectPackageManager(environment = process.env) {
32
+ const userAgent = environment.npm_config_user_agent ?? ""
33
+ const name = userAgent.split("/", 1)[0]
34
+
35
+ return ["pnpm", "npm", "yarn", "bun"].includes(name) ? name : "npm"
36
+ }
37
+
38
+ export function projectName(directory) {
39
+ const name = basename(resolve(directory))
40
+ const valid =
41
+ name.length <= 214 &&
42
+ /^(?:[a-z0-9][a-z0-9._-]*)$/.test(name) &&
43
+ !name.startsWith(".") &&
44
+ !name.startsWith("_")
45
+
46
+ if (!valid) {
47
+ throw new Error(
48
+ `${JSON.stringify(name)} is not a valid package name. Use lowercase letters, numbers, dots, hyphens, or underscores.`,
49
+ )
50
+ }
51
+
52
+ return name
53
+ }
54
+
55
+ function parseArguments(arguments_) {
56
+ let install = true
57
+ let directory
58
+
59
+ for (const argument of arguments_) {
60
+ if (argument === "--no-install") {
61
+ install = false
62
+ } else if (argument === "-h" || argument === "--help") {
63
+ return { help: true }
64
+ } else if (argument === "-v" || argument === "--version") {
65
+ return { version: true }
66
+ } else if (argument.startsWith("-")) {
67
+ throw new Error(`Unknown option: ${argument}`)
68
+ } else if (directory === undefined) {
69
+ directory = argument
70
+ } else {
71
+ throw new Error("Provide exactly one target directory.")
72
+ }
73
+ }
74
+
75
+ if (directory === undefined) {
76
+ throw new Error("A target directory is required.")
77
+ }
78
+
79
+ return { directory, install }
80
+ }
81
+
82
+ async function ensureEmptyDirectory(directory) {
83
+ try {
84
+ const details = await stat(directory)
85
+
86
+ if (!details.isDirectory()) {
87
+ throw new Error(`${directory} exists and is not a directory.`)
88
+ }
89
+
90
+ const entries = await readdir(directory)
91
+
92
+ if (entries.length > 0) {
93
+ throw new Error(`${directory} is not empty.`)
94
+ }
95
+ } catch (error) {
96
+ if (error?.code !== "ENOENT") {
97
+ throw error
98
+ }
99
+
100
+ await mkdir(directory, { recursive: true })
101
+ }
102
+ }
103
+
104
+ async function installDependencies(packageManager, directory) {
105
+ await new Promise((fulfill, reject) => {
106
+ const child = spawn(packageManager, ["install"], {
107
+ cwd: directory,
108
+ stdio: "inherit",
109
+ })
110
+
111
+ child.on("error", reject)
112
+ child.on("exit", (code, signal) => {
113
+ if (code === 0) {
114
+ fulfill()
115
+ } else {
116
+ reject(
117
+ new Error(
118
+ signal
119
+ ? `${packageManager} install was terminated by ${signal}.`
120
+ : `${packageManager} install exited with code ${code}.`,
121
+ ),
122
+ )
123
+ }
124
+ })
125
+ })
126
+ }
127
+
128
+ export async function createProject(directory, options = {}) {
129
+ const targetDirectory = resolve(options.cwd ?? process.cwd(), directory)
130
+ const name = projectName(targetDirectory)
131
+ const packageManager = options.packageManager ?? detectPackageManager()
132
+
133
+ await ensureEmptyDirectory(targetDirectory)
134
+ await cp(templateDirectory, targetDirectory, { recursive: true })
135
+ await rename(
136
+ resolve(targetDirectory, "gitignore"),
137
+ resolve(targetDirectory, ".gitignore"),
138
+ )
139
+
140
+ const packageFile = resolve(targetDirectory, "package.json")
141
+ const template = await readFile(packageFile, "utf8")
142
+
143
+ await writeFile(
144
+ packageFile,
145
+ template
146
+ .replaceAll("__PACKAGE_NAME__", name)
147
+ .replaceAll("__FLAMEFRONT_VERSION__", packageMetadata.version),
148
+ )
149
+
150
+ if (options.install !== false) {
151
+ await (options.installDependencies ?? installDependencies)(
152
+ packageManager,
153
+ targetDirectory,
154
+ )
155
+ }
156
+
157
+ return { name, packageManager, targetDirectory }
158
+ }
159
+
160
+ export async function run(arguments_, options = {}) {
161
+ try {
162
+ const parsed = parseArguments(arguments_)
163
+
164
+ if (parsed.help) {
165
+ console.log(usage)
166
+ return
167
+ }
168
+
169
+ if (parsed.version) {
170
+ console.log(packageMetadata.version)
171
+ return
172
+ }
173
+
174
+ const result = await createProject(parsed.directory, {
175
+ cwd: options.cwd,
176
+ install: parsed.install,
177
+ installDependencies: options.installDependencies,
178
+ packageManager: options.packageManager,
179
+ })
180
+ const relativeTarget = parsed.directory === "." ? "." : parsed.directory
181
+
182
+ console.log(`Created ${result.name} in ${result.targetDirectory}.`)
183
+ if (!parsed.install) {
184
+ console.log(
185
+ `\nNext steps:\n cd ${relativeTarget}\n ${result.packageManager} install\n ${result.packageManager} run dev`,
186
+ )
187
+ } else {
188
+ console.log(
189
+ `\nNext steps:\n cd ${relativeTarget}\n ${result.packageManager} run dev`,
190
+ )
191
+ }
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error)
194
+
195
+ console.error(`create-flamefront: ${message}`)
196
+ process.exitCode = 1
197
+ }
198
+ }
@@ -0,0 +1,3 @@
1
+ .flamefront/
2
+ dist/
3
+ node_modules/
@@ -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>Flamefront app</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=26.0.0"
7
+ },
8
+ "scripts": {
9
+ "dev": "ff dev",
10
+ "build": "ff build",
11
+ "preview": "ff preview",
12
+ "routes": "ff routes",
13
+ "typegen": "ff typegen",
14
+ "typecheck": "tsc"
15
+ },
16
+ "dependencies": {
17
+ "@octanejs/remix-router": "0.1.48",
18
+ "@octanejs/vite-plugin": "0.1.54",
19
+ "flamefront": "^__FLAMEFRONT_VERSION__",
20
+ "octane": "0.2.7"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "26.5.1",
24
+ "typescript": "5.9.3",
25
+ "vite": "8.3.0"
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ esbuild: true
@@ -0,0 +1,8 @@
1
+ import { Outlet } from "@octanejs/remix-router"
2
+
3
+ export default function AppShell() @{
4
+ <div className="app-shell">
5
+ <header>Flamefront</header>
6
+ <Outlet />
7
+ </div>
8
+ }
@@ -0,0 +1,16 @@
1
+ import { useLoaderData } from "@octanejs/remix-router"
2
+ import type { LoaderArgs } from "flamefront/server"
3
+
4
+ export async function loader({ request }: LoaderArgs<"/">) {
5
+ return { pathname: new URL(request.url).pathname }
6
+ }
7
+
8
+ export default function HomePage() @{
9
+ const data = useLoaderData<typeof loader>()
10
+
11
+ <main>
12
+ <p className="eyebrow">Server-rendered with Flamefront</p>
13
+ <h1>Your app is ready.</h1>
14
+ <p>The loader handled {data.pathname}</p>
15
+ </main>
16
+ }
@@ -0,0 +1,6 @@
1
+ import { defineApp, serverRoute } from "flamefront"
2
+
3
+ export const app = defineApp({
4
+ shell: "/src/AppShell.tsrx",
5
+ routes: [serverRoute("/", "/src/HomePage.tsrx")],
6
+ })
@@ -0,0 +1,16 @@
1
+ import { importRoute } from "virtual:flamefront/server-routes"
2
+ import { createServerEntry } from "flamefront/entry"
3
+ import { createOctaneDocuments } from "flamefront/octane"
4
+ import { createRouteRuntime } from "flamefront/server"
5
+ import { app } from "./app.ts"
6
+
7
+ const runtime = createRouteRuntime({ app, importRoute })
8
+ const documents = createOctaneDocuments({ app, runtime })
9
+
10
+ export default createServerEntry({
11
+ app,
12
+ documents,
13
+ assets: {
14
+ clientDirectory: new URL("../client/", import.meta.url),
15
+ },
16
+ })
@@ -0,0 +1,27 @@
1
+ declare module "*.tsrx" {
2
+ const Component: unknown
3
+
4
+ export default Component
5
+ }
6
+
7
+ declare module "virtual:flamefront/remix-routes" {
8
+ import type { RouteObject } from "@octanejs/remix-router"
9
+ import type { RouterDocument as RouterDocumentComponent } from "flamefront/octane"
10
+ import type {
11
+ GeneratedRouteMetadata,
12
+ NormalizedRoutingOptions,
13
+ } from "flamefront"
14
+
15
+ export const RouterDocument: RouterDocumentComponent
16
+ export const routes: RouteObject[]
17
+ export const routing: NormalizedRoutingOptions
18
+ export const routeMetadata: readonly GeneratedRouteMetadata[]
19
+ export function preloadRoute(entry: string): Promise<void>
20
+ }
21
+
22
+ declare module "virtual:flamefront/server-routes" {
23
+ import type { RouteModule } from "flamefront/server"
24
+
25
+ export function importRoute(entry: string): Promise<RouteModule>
26
+ export function loadActions(): Promise<void>
27
+ }
@@ -0,0 +1,5 @@
1
+ import { startOctaneClient } from "flamefront/octane/client"
2
+ import { app } from "./app.ts"
3
+ import "./styles.css"
4
+
5
+ await startOctaneClient({ app })
@@ -0,0 +1,31 @@
1
+ :root {
2
+ color: #231a14;
3
+ background: #fff8ed;
4
+ font-family: ui-sans-serif, system-ui, sans-serif;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ }
10
+
11
+ .app-shell {
12
+ width: min(42rem, calc(100% - 3rem));
13
+ margin: 0 auto;
14
+ padding: 3rem 0;
15
+ }
16
+
17
+ header,
18
+ .eyebrow {
19
+ color: #b33a16;
20
+ font-weight: 700;
21
+ }
22
+
23
+ main {
24
+ padding-top: 6rem;
25
+ }
26
+
27
+ h1 {
28
+ margin: 0.25rem 0;
29
+ font-size: clamp(2.5rem, 8vw, 5rem);
30
+ line-height: 0.95;
31
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowImportingTsExtensions": true,
4
+ "jsx": "react-jsx",
5
+ "jsxImportSource": "octane",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "target": "ESNext",
12
+ "types": ["node", "vite/client"]
13
+ },
14
+ "include": ["src", ".flamefront/types"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { octane } from "@octanejs/vite-plugin"
2
+ import { flamefront } from "flamefront/vite"
3
+ import { defineConfig } from "vite"
4
+
5
+ export default defineConfig({
6
+ plugins: [flamefront({ target: "node" }), octane()],
7
+ })
package/readme.md DELETED
@@ -1 +0,0 @@
1
- Coming soon...