xpine 0.0.44 → 0.0.45
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 +328 -55
- package/dist/index.js +1 -1
- package/dist/src/express.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,64 +1,308 @@
|
|
|
1
1
|
# XPine - Alpine.js + JSX + Tailwind framework
|
|
2
2
|
|
|
3
|
-
Combines JSX with Alpine.js for a simpler, easier development experience.
|
|
3
|
+
Combines JSX with Alpine.js for a simpler, easier development experience. Includes a static site generator.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`npm install xpine`
|
|
8
|
+
|
|
9
|
+
### Routing, page setup, and using Alpine.js
|
|
10
|
+
|
|
11
|
+
XPine uses page based routing. Render an HTML page using JSX components, for example the path `/src/page/about.tsx` will route to `/about` and `/src/page/index.tsx` will route to `/`
|
|
8
12
|
|
|
9
13
|
```
|
|
10
|
-
|
|
14
|
+
import { WrapperProps } from 'xpine/dist/types';
|
|
15
|
+
import Base from '../components/Base';
|
|
16
|
+
import Navbar from '../components/Navbar';
|
|
17
|
+
|
|
18
|
+
export const config = {
|
|
19
|
+
data() {
|
|
20
|
+
return {
|
|
21
|
+
title: 'Home page',
|
|
22
|
+
description: 'The description',
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
wrapper({ req, children, data, routePath }: WrapperProps) {
|
|
26
|
+
return (
|
|
27
|
+
<Base
|
|
28
|
+
title={data?.title || 'My awesome website'}
|
|
29
|
+
description={data?.description}
|
|
30
|
+
req={req}
|
|
31
|
+
>
|
|
32
|
+
<h1>Home page wrapper</h1>
|
|
33
|
+
{children}
|
|
34
|
+
</Base>
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function Home() {
|
|
40
|
+
return (
|
|
41
|
+
<div x-data="HomePageData" x-on:click="logMessage">
|
|
42
|
+
Hello world
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
<script />
|
|
48
|
+
|
|
49
|
+
export function HomePageData() {
|
|
50
|
+
return {
|
|
51
|
+
logMessage() {
|
|
52
|
+
console.log('Hello world');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
11
56
|
```
|
|
12
57
|
|
|
13
|
-
|
|
58
|
+
- src/components/Base.tsx
|
|
14
59
|
|
|
15
|
-
|
|
60
|
+
```
|
|
61
|
+
import { JsxElement } from 'typescript';
|
|
62
|
+
import { ServerRequest } from 'xpine/dist/types';
|
|
16
63
|
|
|
17
|
-
|
|
64
|
+
type BaseProps = {
|
|
65
|
+
head?: JsxElement;
|
|
66
|
+
title: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
req?: ServerRequest;
|
|
69
|
+
children?: JsxElement;
|
|
70
|
+
}
|
|
18
71
|
|
|
19
|
-
|
|
72
|
+
export default async function Base({
|
|
73
|
+
head,
|
|
74
|
+
title,
|
|
75
|
+
description,
|
|
76
|
+
children,
|
|
77
|
+
}: BaseProps) {
|
|
78
|
+
return (
|
|
79
|
+
<html lang="en">
|
|
80
|
+
<head>
|
|
81
|
+
<meta charset="UTF-8" />
|
|
82
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
83
|
+
<meta name="robots" content="index,follow" />
|
|
84
|
+
<meta name="description" content={description || `${title} page`} />
|
|
85
|
+
<title>{title || 'My Website'}</title>
|
|
86
|
+
<link rel="stylesheet" href="/styles/global.css" />
|
|
87
|
+
<script defer src="/scripts/app.js"></script>
|
|
88
|
+
{head}
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div id="xpine-root">
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
20
99
|
|
|
21
|
-
|
|
100
|
+
### Client side Javascript and CSS files
|
|
22
101
|
|
|
23
|
-
|
|
102
|
+
In the above code, we have a `/styles/global.css` file and a `/scripts/app.js` file import.
|
|
24
103
|
|
|
25
|
-
|
|
104
|
+
#### CSS
|
|
26
105
|
|
|
27
|
-
|
|
106
|
+
Create a file called `/src/public/styles/global.css` and import Tailwind:
|
|
28
107
|
|
|
29
|
-
|
|
108
|
+
```
|
|
109
|
+
@import "tailwindcss";
|
|
110
|
+
```
|
|
30
111
|
|
|
31
|
-
|
|
112
|
+
Note you must install tailwindcss yourself: `npm install tailwindcss`
|
|
113
|
+
|
|
114
|
+
#### Javascript
|
|
115
|
+
|
|
116
|
+
Create a file called `/src/public/scripts/index.ts`:
|
|
32
117
|
|
|
33
118
|
```
|
|
34
|
-
import
|
|
119
|
+
import Alpine from 'alpinejs';
|
|
35
120
|
|
|
36
|
-
|
|
121
|
+
Alpine.start();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Note you must install alpinejs yourself: `npm install alpinejs`
|
|
125
|
+
|
|
126
|
+
### Dynamic routing
|
|
127
|
+
|
|
128
|
+
Create dynamic routes with paths similar to this `/src/pages/[pathA]/[pathB]`
|
|
129
|
+
|
|
130
|
+
### Express API endpoints
|
|
37
131
|
|
|
132
|
+
Create regular Express routes by using .ts file extensions in the `/src/pages` folder. Specify the HTTP method by naming the file something like `/src/pages/api/my-endpoint.POST.ts`
|
|
38
133
|
```
|
|
134
|
+
import { PageProps } from 'xpine/dist/types';
|
|
39
135
|
|
|
40
|
-
|
|
136
|
+
export default async function myEndpoint({ res }: PageProps) {
|
|
137
|
+
res.status(200).json({
|
|
138
|
+
message: 'Hello World!',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
```
|
|
41
142
|
|
|
42
|
-
|
|
143
|
+
### Catch all routes
|
|
144
|
+
You can create catch all routes by naming the file \_all\_.(jsx|tsx|js|ts). You can make static catch all route pages by using the param `0` in the staticPaths config function:
|
|
145
|
+
```
|
|
146
|
+
export const config = {
|
|
147
|
+
staticPaths() {
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
0: 'hello/world',
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
```
|
|
43
156
|
|
|
44
|
-
|
|
157
|
+
You can get the route param in your function with req.params[0], such as how express handles catch all routes.
|
|
45
158
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
159
|
+
### Static Site Generation
|
|
160
|
+
|
|
161
|
+
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:
|
|
162
|
+
```
|
|
163
|
+
export const config = {
|
|
164
|
+
staticPaths: true,
|
|
165
|
+
data() {
|
|
166
|
+
return {
|
|
167
|
+
title: 'My title'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
or a `+config.ts` file.
|
|
174
|
+
|
|
175
|
+
### Configs
|
|
176
|
+
|
|
177
|
+
Configs can be nested. Create a +config.ts file in a directory and all subfolders will inherit that config unless overridden by their own +config.ts files. Want to apply static paths to an entire folder except for a single folder? In that folder you can create a `+config.ts` file like this:
|
|
178
|
+
```
|
|
179
|
+
export default {
|
|
180
|
+
staticPaths: false,
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Dynamic Static Pages
|
|
185
|
+
|
|
186
|
+
You can also create dynamic static pages by using a function in the staticPaths folder. For example, a directory named `/src/[pathA]/[pathB]/[pathC]/[pathD].tsx` might have a configuration file like this:
|
|
187
|
+
```
|
|
188
|
+
import { ServerRequest } from 'xpine/dist/types';
|
|
189
|
+
import axios from 'axios';
|
|
190
|
+
|
|
191
|
+
export const config = {
|
|
192
|
+
staticPaths() {
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
pathA: 'my-path-a2',
|
|
196
|
+
pathB: 'my-path-b2',
|
|
197
|
+
pathC: 'my-path-c2',
|
|
198
|
+
pathD: '2'
|
|
58
199
|
}
|
|
59
|
-
|
|
60
|
-
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
async data(req: ServerRequest) {
|
|
203
|
+
const url = `https://jsonplaceholder.typicode.com/posts/${req.params.pathD}`;
|
|
204
|
+
try {
|
|
205
|
+
const { data } = await axios.get(url);
|
|
206
|
+
return {
|
|
207
|
+
...data,
|
|
208
|
+
...req.params,
|
|
209
|
+
};
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('could not fetch', url);
|
|
212
|
+
return {
|
|
213
|
+
...req.params,
|
|
214
|
+
data: {},
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Context
|
|
222
|
+
|
|
223
|
+
Create app context, useful for things like Navbars. In `/src/context.tsx`:
|
|
224
|
+
```
|
|
225
|
+
import { createContext } from 'xpine';
|
|
226
|
+
|
|
227
|
+
export function NavbarContext() {
|
|
228
|
+
const navbar = createContext([]);
|
|
229
|
+
return {
|
|
230
|
+
navbar,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
61
234
|
|
|
235
|
+
then in a page, say `/src/pages/about.tsx`, you can add to the NavbarContext like this:
|
|
236
|
+
```
|
|
237
|
+
export function xpineOnLoad() {
|
|
238
|
+
context.addToArray('navbar', 'My awesome context 1', 2);
|
|
239
|
+
context.addToArray('navbar', 'My awesome context 2', new Date('January 11, 2024'));
|
|
240
|
+
context.addToArray('navbar', 'My awesome context 3', new Date('January 10, 2024'));
|
|
241
|
+
context.addToArray('navbar', 'My awesome context 4', new Date('January 30, 2024'));
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Context is sorted by array position then by date. You can then use context in your component like this:
|
|
246
|
+
```
|
|
247
|
+
import { context } from 'xpine';
|
|
248
|
+
|
|
249
|
+
export default function Navbar() {
|
|
250
|
+
const navbar = context.get('navbar');
|
|
251
|
+
return (
|
|
252
|
+
<div>
|
|
253
|
+
{navbar.map(item => {
|
|
254
|
+
return <div>{item}</div>
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Server set up
|
|
262
|
+
|
|
263
|
+
- src/server/app.ts
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
import express from 'express';
|
|
267
|
+
import cookieParser from 'cookie-parser';
|
|
268
|
+
import logger from 'morgan';
|
|
269
|
+
import http from 'http';
|
|
270
|
+
import { createXPineRouter, setupEnv } from 'xpine';
|
|
271
|
+
|
|
272
|
+
await setupEnv();
|
|
273
|
+
|
|
274
|
+
export default async function startServer() {
|
|
275
|
+
const port = process.env.PORT || 8080;
|
|
276
|
+
const app = express();
|
|
277
|
+
app.set('view engine', 'html');
|
|
278
|
+
|
|
279
|
+
app.enable('trust proxy');
|
|
280
|
+
app.use(logger('dev'));
|
|
281
|
+
app.use(express.json());
|
|
282
|
+
app.use(express.urlencoded({ extended: true, }));
|
|
283
|
+
app.use(cookieParser());
|
|
284
|
+
|
|
285
|
+
await createXPineRouter(app);
|
|
286
|
+
|
|
287
|
+
app.set('port', port);
|
|
288
|
+
const server = http.createServer(app);
|
|
289
|
+
server.listen(port, () => {
|
|
290
|
+
console.info(`Server listening on port ${port}`);
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
app,
|
|
294
|
+
server,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### xpine.config.mjs file
|
|
300
|
+
|
|
301
|
+
Add an xpine.config.mjs file to your root directory. This is used primarily for configuring file paths
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
export default {}
|
|
305
|
+
```
|
|
62
306
|
|
|
63
307
|
### SPA interactivity
|
|
64
308
|
|
|
@@ -69,29 +313,58 @@ setupEnv also supports AWS secrets manager. Simply add SECRETS_NAME=your_aws_sec
|
|
|
69
313
|
- data-spa-crossorigin="true"
|
|
70
314
|
- enables cross-origin spa navigation requests (untested)
|
|
71
315
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
316
|
+
## API
|
|
317
|
+
|
|
318
|
+
### Build command
|
|
319
|
+
|
|
320
|
+
`xpine-build`
|
|
321
|
+
|
|
322
|
+
### Dev server command
|
|
323
|
+
|
|
324
|
+
`xpine-dev`
|
|
325
|
+
|
|
326
|
+
### Auth
|
|
327
|
+
|
|
328
|
+
`import { signUser, verifyUser } from 'xpine';`
|
|
329
|
+
|
|
330
|
+
### Config
|
|
331
|
+
|
|
332
|
+
`import { config } from 'xpine';`
|
|
333
|
+
|
|
334
|
+
### Env
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
import { setupEnv } from 'xpine';
|
|
338
|
+
|
|
339
|
+
await setupEnv();
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
setupEnv also supports AWS secrets manager. Simply add SECRETS_NAME=your_aws_secret_name to your .env.{stage} file
|
|
344
|
+
|
|
345
|
+
### Custom events
|
|
346
|
+
- spa-update-page-content
|
|
347
|
+
- sent when the page content has update
|
|
348
|
+
- spa-update-page-url
|
|
349
|
+
- sent when the page URL has update
|
|
350
|
+
- spa-link-click
|
|
351
|
+
- sent when link initially gets clicked and when link is done updating content
|
|
352
|
+
- the "state" value in the event detail will be "start" or "end"
|
|
353
|
+
- breakpoint-change
|
|
354
|
+
- sent when a breakpoint is changed via your Tailwind css file's breakpoints in the @theme directive, e.g.
|
|
355
|
+
```
|
|
356
|
+
@theme {
|
|
357
|
+
--breakpoint-xl: 1184px;
|
|
358
|
+
--breakpoint-sm: 640px;
|
|
359
|
+
--breakpoint-md: 768px;
|
|
360
|
+
--breakpoint-lg: 1024px;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
- this will send a custom event with the detail being the breakpoint, such as `{ breakpoint: 'xl' }`
|
|
364
|
+
- note this will only send an event based on the values of the breakpoints in your @theme configuration
|
|
92
365
|
|
|
93
|
-
|
|
366
|
+
### Custom scripts for certain pages
|
|
94
367
|
|
|
95
368
|
1. Add script to src/public/scripts/pages/your_script.ts
|
|
96
|
-
2. Import script into page HTML (e.g.
|
|
369
|
+
2. Import script into page HTML (e.g. `<script src="/scripts/pages/your_script.ts">`)
|
|
97
370
|
3. To unload event listeners, use `window.addEventListener('spa-update-page-url', () => { remove event listeners here})` in the code
|
package/dist/index.js
CHANGED
|
@@ -512,7 +512,7 @@ function getAllBuiltRoutes() {
|
|
|
512
512
|
async function createRouteFunction(route, configFiles) {
|
|
513
513
|
const isJSX = route.originalRoute.endsWith(".tsx") || route.originalRoute.endsWith(".jsx");
|
|
514
514
|
const slugRoute = route.route.replace(/[ ]/g, "");
|
|
515
|
-
const foundMethod = methods.find((method) => slugRoute.endsWith(`.${method}`));
|
|
515
|
+
const foundMethod = methods.find((method) => slugRoute.toUpperCase().endsWith(`.${method.toUpperCase()}`));
|
|
516
516
|
const isDynamicRoute = slugRoute.match(regex_default.isDynamicRoute);
|
|
517
517
|
let formattedRouteItem = slugRoute;
|
|
518
518
|
if (foundMethod) formattedRouteItem = formattedRouteItem.split(".").shift();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,EAAqB,MAAM,SAAS,CAAC;AAsJ5E,wBAAsB,YAAY;;;GA8BjC;AAiBD,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,iBAyB1F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,KAAK,CAanG;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,OAAc,UAQzE"}
|