xpine 0.0.58 → 0.0.60
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/.claude/settings.local.json +2 -1
- package/.claude/worktrees/happy-lalande-c6c39c/.claude/settings.local.json +33 -0
- package/.claude/worktrees/happy-lalande-c6c39c/.d.ts +1 -0
- package/.claude/worktrees/happy-lalande-c6c39c/.gitattributes +2 -0
- package/.claude/worktrees/happy-lalande-c6c39c/README.md +571 -0
- package/.claude/worktrees/happy-lalande-c6c39c/TODO +2 -0
- package/.claude/worktrees/happy-lalande-c6c39c/eslint.config.mjs +27 -0
- package/.claude/worktrees/happy-lalande-c6c39c/jsx-runtime.d.ts +1 -0
- package/.claude/worktrees/happy-lalande-c6c39c/package-lock.json +5862 -0
- package/.claude/worktrees/happy-lalande-c6c39c/package.json +62 -0
- package/.claude/worktrees/happy-lalande-c6c39c/tsconfig.json +43 -0
- package/.claude/worktrees/happy-lalande-c6c39c/types.ts +55 -0
- package/.claude/worktrees/happy-lalande-c6c39c/xpine.config.mjs +1 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/.claude/settings.local.json +33 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/.d.ts +1 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/.gitattributes +2 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/README.md +571 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/TODO +2 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/eslint.config.mjs +27 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/jsx-runtime.d.ts +1 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/package-lock.json +5862 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/package.json +62 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/tsconfig.json +43 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/types.ts +55 -0
- package/.claude/worktrees/jolly-hofstadter-2764be/xpine.config.mjs +1 -0
- package/README.md +104 -0
- package/create-xpine-app/README.md +20 -0
- package/create-xpine-app/index.js +80 -0
- package/create-xpine-app/package.json +30 -0
- package/create-xpine-app/template/README.md +50 -0
- package/create-xpine-app/template/eslint.config.mjs +27 -0
- package/create-xpine-app/template/gitignore +5 -0
- package/create-xpine-app/template/package.json +32 -0
- package/create-xpine-app/template/tsconfig.json +36 -0
- package/create-xpine-app/template/xpine.config.mjs +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +269 -106
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/express.d.ts +5 -0
- package/dist/src/express.d.ts.map +1 -1
- package/dist/src/scripts/build.d.ts.map +1 -1
- package/dist/src/scripts/xpine-build.js +6 -2
- package/dist/src/scripts/xpine-dev.js +15 -21
- package/dist/src/util/html.d.ts +9 -1
- package/dist/src/util/html.d.ts.map +1 -1
- package/dist/src/util/regex.d.ts +2 -0
- package/dist/src/util/regex.d.ts.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/eslint.config.mjs +1 -1
- package/package.json +1 -1
- package/types.ts +5 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xpine",
|
|
3
|
+
"version": "0.0.59",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
|
7
|
+
"@tailwindcss/postcss": "^4.1.12",
|
|
8
|
+
"builtin-modules": "^4.0.0",
|
|
9
|
+
"chokidar": "^4.0.3",
|
|
10
|
+
"dotenv": "^16.4.7",
|
|
11
|
+
"esbuild": "^0.25.0",
|
|
12
|
+
"express": "^4.21.2",
|
|
13
|
+
"express-ws": "^5.0.2",
|
|
14
|
+
"fs-extra": "^11.3.0",
|
|
15
|
+
"glob": "^11.0.1",
|
|
16
|
+
"jsonwebtoken": "^9.0.2",
|
|
17
|
+
"micromatch": "^4.0.8",
|
|
18
|
+
"minify-xml": "^4.5.2",
|
|
19
|
+
"postcss": "^8.5.3",
|
|
20
|
+
"request-ip": "^3.3.0",
|
|
21
|
+
"shelljs": "^0.9.2",
|
|
22
|
+
"tsx": "^4.19.3",
|
|
23
|
+
"typescript": "^5.7.3",
|
|
24
|
+
"yargs": "^17.7.2"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git://github.com/dan-caldwell/xpine.git"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsx ./src/scripts/run-build-module.ts && npm run build:types",
|
|
32
|
+
"build:types": "sh ./src/scripts/build-types.sh",
|
|
33
|
+
"xpine-dev-server": "tsx ./src/scripts/xpine-dev-server.ts",
|
|
34
|
+
"test": "cd ./tests && npm run test",
|
|
35
|
+
"test:dev": "cd ./tests && npm run dev",
|
|
36
|
+
"test:start": "cd ./tests && npm run build && npm start",
|
|
37
|
+
"test:build": "cd ./tests && npm run build",
|
|
38
|
+
"test:build:dev": "cd ./tests && npm run build:dev",
|
|
39
|
+
"lint": "npx eslint",
|
|
40
|
+
"fix": "npx eslint --fix",
|
|
41
|
+
"release:patch": "npm run build && npm version patch && npm publish"
|
|
42
|
+
},
|
|
43
|
+
"bin": {
|
|
44
|
+
"xpine-build": "dist/src/scripts/xpine-build.js",
|
|
45
|
+
"xpine-dev": "dist/src/scripts/xpine-dev.js"
|
|
46
|
+
},
|
|
47
|
+
"types": "dist/index.d.ts",
|
|
48
|
+
"type": "module",
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.24.0",
|
|
51
|
+
"@tailwindcss/oxide-darwin-arm64": "^4.1.3",
|
|
52
|
+
"@types/express": "^5.0.0",
|
|
53
|
+
"@types/micromatch": "^4.0.9",
|
|
54
|
+
"@types/node": "^22.13.5",
|
|
55
|
+
"@types/request-ip": "^0.0.41",
|
|
56
|
+
"@types/shelljs": "^0.8.15",
|
|
57
|
+
"@types/yargs": "^17.0.33",
|
|
58
|
+
"axios": "^1.8.4",
|
|
59
|
+
"lightningcss-darwin-arm64": "^1.29.3",
|
|
60
|
+
"typescript-eslint": "^8.29.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "preserve",
|
|
4
|
+
"jsxFragmentFactory": "html.fragment",
|
|
5
|
+
"jsxFactory": "html.createElement",
|
|
6
|
+
"lib": [
|
|
7
|
+
"dom",
|
|
8
|
+
"dom.iterable",
|
|
9
|
+
"esnext"
|
|
10
|
+
],
|
|
11
|
+
"allowJs": false,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": false,
|
|
14
|
+
"forceConsistentCasingInFileNames": false,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"target": "esnext",
|
|
18
|
+
"module": "ESNext",
|
|
19
|
+
"moduleResolution": "node",
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"jsxImportSource": "xpine",
|
|
23
|
+
"incremental": true,
|
|
24
|
+
"allowImportingTsExtensions": true,
|
|
25
|
+
// Generate d.ts files
|
|
26
|
+
"declaration": true,
|
|
27
|
+
"emitDeclarationOnly": true,
|
|
28
|
+
"outDir": "dist",
|
|
29
|
+
"declarationMap": true,
|
|
30
|
+
"types": []
|
|
31
|
+
},
|
|
32
|
+
"include": [
|
|
33
|
+
"src/**/*.ts"
|
|
34
|
+
],
|
|
35
|
+
"exclude": [
|
|
36
|
+
"node_modules",
|
|
37
|
+
"./node_modules",
|
|
38
|
+
"./node_modules/*",
|
|
39
|
+
"./node_modules/@types/node/index.d.ts",
|
|
40
|
+
"dist",
|
|
41
|
+
"**/*.js"
|
|
42
|
+
],
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
export type XPineConfig = {
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TokenUser = {
|
|
9
|
+
email?: string;
|
|
10
|
+
username?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ServerRequest = Request & {
|
|
14
|
+
user?: TokenUser;
|
|
15
|
+
clientIp?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WrapperProps = {
|
|
19
|
+
req: ServerRequest;
|
|
20
|
+
children: any;
|
|
21
|
+
config: ConfigFile;
|
|
22
|
+
data?: any;
|
|
23
|
+
routePath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ConfigFile = {
|
|
27
|
+
staticPaths?: boolean | (() => Promise<{ [key: string]: string }[]> | { [key: string]: string }[]);
|
|
28
|
+
wrapper?: (props: WrapperProps) => Promise<any>;
|
|
29
|
+
data?: (req: ServerRequest) => Promise<any>;
|
|
30
|
+
routeMiddleware?: (req: ServerRequest, res: Response, next: NextFunction) => void;
|
|
31
|
+
// For multi-segment dynamic routes ([...slug]): validate a slug that was not
|
|
32
|
+
// generated at build time so it can be resolved safely at request time.
|
|
33
|
+
// Return false (the default for unknown slugs) to fall through to the 404 handler.
|
|
34
|
+
isValid?: (slug: string, req: ServerRequest) => boolean | Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PageProps = {
|
|
38
|
+
req: ServerRequest;
|
|
39
|
+
res: Response;
|
|
40
|
+
data: any;
|
|
41
|
+
routePath: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type FileItem = {
|
|
45
|
+
file: string;
|
|
46
|
+
size: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ComponentData = {
|
|
50
|
+
path: string;
|
|
51
|
+
contents: string;
|
|
52
|
+
clientContent: string;
|
|
53
|
+
configFiles: string[];
|
|
54
|
+
source: ts.SourceFile;
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
package/README.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Combines JSX with Alpine.js for a simpler, easier development experience. Includes a static site generator.
|
|
4
4
|
|
|
5
|
+
### Quick start
|
|
6
|
+
|
|
7
|
+
Scaffold a new app with pages, demo content, and an API endpoint already set up:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx create-xpine-app my-app
|
|
11
|
+
cd my-app
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
5
16
|
### Install
|
|
6
17
|
|
|
7
18
|
`npm install xpine`
|
|
@@ -155,6 +166,47 @@ You can create catch all routes by naming the file \_all\_.(jsx|tsx|js|ts). You
|
|
|
155
166
|
|
|
156
167
|
You can get the route param in your function with req.params[0], such as how express handles catch all routes.
|
|
157
168
|
|
|
169
|
+
### Multi-segment dynamic routes (`[...slug]`)
|
|
170
|
+
|
|
171
|
+
Catch all (`_all_`) routes register a single Express wildcard, which means they match _every_ path under their prefix — including paths that should not exist. For deeply nested but known dynamic routes (e.g. a blog post at `/blog/technology/devops/my-blog-post`), use a multi-segment dynamic param instead. Name the file `[...slug].(jsx|tsx|js|ts)`; the captured value (which may contain slashes) is available as `req.params.slug`.
|
|
172
|
+
|
|
173
|
+
Instead of a wildcard, XPine registers an **explicit Express route for each slug** returned by the `staticPaths` config function, so unknown paths safely fall through to your 404 page:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
// /src/pages/blog/+config.ts
|
|
177
|
+
export default {
|
|
178
|
+
staticPaths() {
|
|
179
|
+
// Slugs can come from a CMS/database
|
|
180
|
+
return [
|
|
181
|
+
{ slug: 'technology/devops/my-blog-post' },
|
|
182
|
+
];
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
// /src/pages/blog/[...slug].tsx
|
|
189
|
+
import { PageProps } from 'xpine/dist/types';
|
|
190
|
+
|
|
191
|
+
export default function BlogPost({ req }: PageProps) {
|
|
192
|
+
return <div>{req.params.slug}</div>;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Each slug from `staticPaths` is also statically generated at build time. To allow slugs that are not known at build time (without falling back to an unsafe catch-all), add an `isValid` function to the config. It receives the requested slug and runs at request time; return `false` (the default for unknown slugs) to fall through to the 404 handler:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
export default {
|
|
200
|
+
staticPaths() {
|
|
201
|
+
return [{ slug: 'technology/devops/my-blog-post' }];
|
|
202
|
+
},
|
|
203
|
+
// Resolve newly-published slugs without a rebuild
|
|
204
|
+
async isValid(slug, req) {
|
|
205
|
+
return await postExists(slug);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
158
210
|
### Route specific middleware
|
|
159
211
|
|
|
160
212
|
If you need route specific middleware, e.g. for file uploads, you can specify a `routeMiddleware` function in a config variable in the endpoint file:
|
|
@@ -168,6 +220,58 @@ export const config = {
|
|
|
168
220
|
}
|
|
169
221
|
```
|
|
170
222
|
|
|
223
|
+
### CSRF protection
|
|
224
|
+
|
|
225
|
+
Because authentication is cookie based, state-changing requests (POST/PUT/PATCH/DELETE) are exposed to cross-site request forgery. Enable the built-in guard by setting `csrf` in your `xpine.config.mjs`:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
export default {
|
|
229
|
+
csrf: true,
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
It uses a stateless **signed double-submit cookie**. On safe requests (GET/HEAD/OPTIONS) it sets a `csrfToken` cookie (readable by client JS). On state-changing requests it requires that same token to be echoed back in an `x-csrf-token` header (or a `_csrf` form field) — a cross-site attacker can make the browser send the cookie but cannot read it or set the header, so forged requests are rejected with a 403. The token is HMAC-signed so it can't be forged even by an attacker who can plant a cookie.
|
|
234
|
+
|
|
235
|
+
Set a secret via the `CSRF_SECRET` env var (it falls back to `JWT_PRIVATE_KEY`). The server fails to start if neither is set.
|
|
236
|
+
|
|
237
|
+
For `fetch`/Alpine requests, read the cookie and send it back:
|
|
238
|
+
```
|
|
239
|
+
const token = document.cookie.split('; ').find(c => c.startsWith('csrfToken='))?.split('=')[1];
|
|
240
|
+
await fetch('/api/thing', {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: { 'x-csrf-token': decodeURIComponent(token) },
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
For server-rendered HTML forms, the token is available on `res.locals.csrfToken` (pages receive `res`); render it as a hidden `<input name="_csrf">`.
|
|
246
|
+
|
|
247
|
+
You can customize the cookie/header/field names, cookie attributes, and skip specific path prefixes (e.g. signed webhooks):
|
|
248
|
+
```
|
|
249
|
+
export default {
|
|
250
|
+
csrf: {
|
|
251
|
+
headerName: 'x-csrf-token',
|
|
252
|
+
cookie: { sameSite: 'strict', secure: true },
|
|
253
|
+
ignorePaths: ['/api/webhooks/'],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### HTML escaping (XSS protection)
|
|
259
|
+
|
|
260
|
+
Values interpolated into JSX are HTML-escaped by default, so rendering user input is safe:
|
|
261
|
+
```
|
|
262
|
+
// req.params.slug = '<img src=x onerror=alert(1)>'
|
|
263
|
+
<div>{req.params.slug}</div> // renders <img src=x onerror=alert(1)>
|
|
264
|
+
```
|
|
265
|
+
Nested components and elements are not re-escaped, and attribute values are escaped too. Text inside `<script>`/`<style>` is left raw (it isn't HTML) — never interpolate untrusted data there.
|
|
266
|
+
|
|
267
|
+
If you have trusted HTML that should render as-is, opt out explicitly with `raw()`:
|
|
268
|
+
```
|
|
269
|
+
import { raw } from 'xpine';
|
|
270
|
+
|
|
271
|
+
<div>{raw(trustedHtmlString)}</div>
|
|
272
|
+
```
|
|
273
|
+
Only use `raw()` with HTML you control — never with user input.
|
|
274
|
+
|
|
171
275
|
### Static Site Generation
|
|
172
276
|
|
|
173
277
|
Generate path specific static pages by specifying in the config of either the page's file, such as `/src/pages/about.tsx` with a config export:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# create-xpine-app
|
|
2
|
+
|
|
3
|
+
Scaffold a new [XPine](https://github.com/dan-caldwell/xpine) app (Alpine.js + JSX + Tailwind) with one command:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx create-xpine-app my-app
|
|
7
|
+
cd my-app
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run `npx create-xpine-app .` to scaffold into the current directory.
|
|
13
|
+
|
|
14
|
+
The generated app includes:
|
|
15
|
+
|
|
16
|
+
- File-based routing with a shared page wrapper
|
|
17
|
+
- Server-side data fetching from the [JSONPlaceholder API](https://jsonplaceholder.typicode.com) (posts and users pages)
|
|
18
|
+
- A dynamic route (`/posts/[id]`) with static generation for known paths
|
|
19
|
+
- An Express API endpoint (`/api/posts`)
|
|
20
|
+
- Alpine.js interactivity examples and Tailwind CSS styling
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
|
|
7
|
+
const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'template');
|
|
8
|
+
|
|
9
|
+
const helpMessage = `
|
|
10
|
+
Usage: npx create-xpine-app [project-name]
|
|
11
|
+
|
|
12
|
+
Scaffolds a new XPine app in a directory named [project-name].
|
|
13
|
+
Pass "." to scaffold into the current directory.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-h, --help Show this message
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
function exitWithError(message) {
|
|
20
|
+
console.error(`\x1b[31m${message}\x1b[0m`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isValidProjectName(name) {
|
|
25
|
+
return /^[a-z0-9~-][a-z0-9._~-]*$/.test(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isEmptyDir(dir) {
|
|
29
|
+
return fs.readdirSync(dir).filter((file) => file !== '.git').length === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getProjectName() {
|
|
33
|
+
const arg = process.argv[2];
|
|
34
|
+
if (arg) return arg;
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
const answer = await rl.question('Project name: (my-xpine-app) ');
|
|
37
|
+
rl.close();
|
|
38
|
+
return answer.trim() || 'my-xpine-app';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (['-h', '--help'].includes(process.argv[2])) {
|
|
42
|
+
console.log(helpMessage);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const projectName = await getProjectName();
|
|
47
|
+
const useCurrentDir = projectName === '.';
|
|
48
|
+
const targetDir = useCurrentDir ? process.cwd() : path.resolve(process.cwd(), projectName);
|
|
49
|
+
const packageName = useCurrentDir ? path.basename(targetDir) : projectName;
|
|
50
|
+
|
|
51
|
+
if (!isValidProjectName(packageName)) {
|
|
52
|
+
exitWithError(`Invalid project name: "${packageName}". Use lowercase letters, numbers, dashes, dots, and underscores.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(targetDir) && !isEmptyDir(targetDir)) {
|
|
56
|
+
exitWithError(`The directory ${useCurrentDir ? 'you are in' : `"${projectName}"`} already exists and is not empty.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`\nScaffolding XPine app in ${targetDir}...`);
|
|
60
|
+
|
|
61
|
+
fs.cpSync(templateDir, targetDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// npm strips .gitignore files from published packages, so the template ships
|
|
64
|
+
// it as "gitignore" and it gets renamed here
|
|
65
|
+
fs.renameSync(path.join(targetDir, 'gitignore'), path.join(targetDir, '.gitignore'));
|
|
66
|
+
|
|
67
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
68
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
69
|
+
packageJson.name = packageName;
|
|
70
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
71
|
+
|
|
72
|
+
console.log(`
|
|
73
|
+
Done! Next steps:
|
|
74
|
+
${useCurrentDir ? '' : `
|
|
75
|
+
cd ${projectName}`}
|
|
76
|
+
npm install
|
|
77
|
+
npm run dev
|
|
78
|
+
|
|
79
|
+
Your app will be running at http://localhost:3000
|
|
80
|
+
`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-xpine-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a new XPine app with one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-xpine-app": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git://github.com/dan-caldwell/xpine.git",
|
|
19
|
+
"directory": "create-xpine-app"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"xpine",
|
|
23
|
+
"create-xpine-app",
|
|
24
|
+
"alpinejs",
|
|
25
|
+
"jsx",
|
|
26
|
+
"tailwind",
|
|
27
|
+
"scaffold"
|
|
28
|
+
],
|
|
29
|
+
"license": "ISC"
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# My XPine App
|
|
2
|
+
|
|
3
|
+
A starter app built with [XPine](https://github.com/dan-caldwell/xpine) — Alpine.js + JSX + Tailwind. Demo content comes from the [JSONPlaceholder API](https://jsonplaceholder.typicode.com).
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then open http://localhost:3000
|
|
13
|
+
|
|
14
|
+
## Scripts
|
|
15
|
+
|
|
16
|
+
| Command | Description |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `npm run dev` | Start the dev server with live reload |
|
|
19
|
+
| `npm run build` | Build the app for production (includes static page generation) |
|
|
20
|
+
| `npm start` | Run the production build |
|
|
21
|
+
| `npm run lint` | Lint the code |
|
|
22
|
+
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
components/ Shared JSX components (Base layout, Navbar)
|
|
28
|
+
pages/ File-based routes
|
|
29
|
+
+config.tsx Shared page wrapper (applies to all pages in this folder and below)
|
|
30
|
+
index.tsx / — home page with Alpine.js examples
|
|
31
|
+
posts/
|
|
32
|
+
index.tsx /posts — post list fetched server-side
|
|
33
|
+
[id].tsx /posts/:id — dynamic route with static generation for known ids
|
|
34
|
+
users/
|
|
35
|
+
index.tsx /users
|
|
36
|
+
api/
|
|
37
|
+
posts.GET.ts /api/posts — Express API endpoint
|
|
38
|
+
404.tsx Not-found page
|
|
39
|
+
public/
|
|
40
|
+
scripts/ Client-side entry (starts Alpine.js)
|
|
41
|
+
styles/ Global CSS (Tailwind)
|
|
42
|
+
server/ Express server setup
|
|
43
|
+
util/ Shared server-side helpers
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Learn more
|
|
47
|
+
|
|
48
|
+
- Page routing, static generation, API endpoints, and more: [XPine README](https://github.com/dan-caldwell/xpine#readme)
|
|
49
|
+
- Alpine.js docs: https://alpinejs.dev
|
|
50
|
+
- Tailwind CSS docs: https://tailwindcss.com
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
|
|
4
|
+
export default tseslint.config(
|
|
5
|
+
eslint.configs.recommended,
|
|
6
|
+
tseslint.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
rules: {
|
|
9
|
+
'eol-last': 2,
|
|
10
|
+
'quotes': ['error', 'single'],
|
|
11
|
+
'comma-dangle': ['error', {
|
|
12
|
+
'objects': 'always',
|
|
13
|
+
'arrays': 'never',
|
|
14
|
+
'imports': 'never',
|
|
15
|
+
'exports': 'never',
|
|
16
|
+
'functions': 'never',
|
|
17
|
+
}],
|
|
18
|
+
'semi': 'error',
|
|
19
|
+
'@typescript-eslint/ban-ts-comment': 'off',
|
|
20
|
+
'@typescript-eslint/no-require-imports': 'off',
|
|
21
|
+
},
|
|
22
|
+
files: ['src/**/*.{js,mjs,ts,tsx,jsx}'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
ignores: ['dist/'],
|
|
26
|
+
}
|
|
27
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xpine-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "PORT=3000 LOCALHOST=1 xpine-dev",
|
|
8
|
+
"build": "xpine-build --removePreviousBuild",
|
|
9
|
+
"start": "PORT=3000 node ./dist/server/run/prod.js",
|
|
10
|
+
"lint": "npx eslint",
|
|
11
|
+
"fix": "npx eslint --fix"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"alpinejs": "^3.14.8",
|
|
15
|
+
"cookie-parser": "^1.4.7",
|
|
16
|
+
"express": "^4.21.2",
|
|
17
|
+
"morgan": "^1.10.0",
|
|
18
|
+
"tailwindcss": "^4.1.12",
|
|
19
|
+
"xpine": "^0.0.59"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@eslint/js": "^9.24.0",
|
|
23
|
+
"@types/alpinejs": "^3.13.11",
|
|
24
|
+
"@types/cookie-parser": "^1.4.8",
|
|
25
|
+
"@types/express": "^5.0.0",
|
|
26
|
+
"@types/morgan": "^1.9.9",
|
|
27
|
+
"@types/node": "^22.13.5",
|
|
28
|
+
"eslint": "^9.24.0",
|
|
29
|
+
"typescript": "^5.7.3",
|
|
30
|
+
"typescript-eslint": "^8.29.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "preserve",
|
|
4
|
+
"jsxFragmentFactory": "html.fragment",
|
|
5
|
+
"jsxFactory": "html.createElement",
|
|
6
|
+
"jsxImportSource": "xpine",
|
|
7
|
+
"lib": [
|
|
8
|
+
"dom",
|
|
9
|
+
"dom.iterable",
|
|
10
|
+
"esnext"
|
|
11
|
+
],
|
|
12
|
+
"target": "esnext",
|
|
13
|
+
"module": "ESNext",
|
|
14
|
+
"moduleResolution": "node",
|
|
15
|
+
"allowJs": false,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"strict": false,
|
|
18
|
+
"noEmit": true,
|
|
19
|
+
"esModuleInterop": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"forceConsistentCasingInFileNames": false,
|
|
23
|
+
"incremental": true,
|
|
24
|
+
"allowImportingTsExtensions": true
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
"eslint.config.mjs"
|
|
30
|
+
],
|
|
31
|
+
"exclude": [
|
|
32
|
+
"node_modules",
|
|
33
|
+
"dist",
|
|
34
|
+
"**/*.js"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './src/util/env';
|
|
|
4
4
|
export * from './src/util/get-config';
|
|
5
5
|
export * from './src/scripts/build';
|
|
6
6
|
export * from './src/auth';
|
|
7
|
+
export * from './src/csrf';
|
|
7
8
|
export * from './src/util/html';
|
|
8
9
|
export * from './src/context';
|
|
9
10
|
export * from './src/util/web';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC"}
|