xpine 0.0.59 → 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.
Files changed (37) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.claude/worktrees/happy-lalande-c6c39c/.claude/settings.local.json +33 -0
  3. package/.claude/worktrees/happy-lalande-c6c39c/.d.ts +1 -0
  4. package/.claude/worktrees/happy-lalande-c6c39c/.gitattributes +2 -0
  5. package/.claude/worktrees/happy-lalande-c6c39c/README.md +571 -0
  6. package/.claude/worktrees/happy-lalande-c6c39c/TODO +2 -0
  7. package/.claude/worktrees/happy-lalande-c6c39c/eslint.config.mjs +27 -0
  8. package/.claude/worktrees/happy-lalande-c6c39c/jsx-runtime.d.ts +1 -0
  9. package/.claude/worktrees/happy-lalande-c6c39c/package-lock.json +5862 -0
  10. package/.claude/worktrees/happy-lalande-c6c39c/package.json +62 -0
  11. package/.claude/worktrees/happy-lalande-c6c39c/tsconfig.json +43 -0
  12. package/.claude/worktrees/happy-lalande-c6c39c/types.ts +55 -0
  13. package/.claude/worktrees/happy-lalande-c6c39c/xpine.config.mjs +1 -0
  14. package/.claude/worktrees/jolly-hofstadter-2764be/.claude/settings.local.json +33 -0
  15. package/.claude/worktrees/jolly-hofstadter-2764be/.d.ts +1 -0
  16. package/.claude/worktrees/jolly-hofstadter-2764be/.gitattributes +2 -0
  17. package/.claude/worktrees/jolly-hofstadter-2764be/README.md +571 -0
  18. package/.claude/worktrees/jolly-hofstadter-2764be/TODO +2 -0
  19. package/.claude/worktrees/jolly-hofstadter-2764be/eslint.config.mjs +27 -0
  20. package/.claude/worktrees/jolly-hofstadter-2764be/jsx-runtime.d.ts +1 -0
  21. package/.claude/worktrees/jolly-hofstadter-2764be/package-lock.json +5862 -0
  22. package/.claude/worktrees/jolly-hofstadter-2764be/package.json +62 -0
  23. package/.claude/worktrees/jolly-hofstadter-2764be/tsconfig.json +43 -0
  24. package/.claude/worktrees/jolly-hofstadter-2764be/types.ts +55 -0
  25. package/.claude/worktrees/jolly-hofstadter-2764be/xpine.config.mjs +1 -0
  26. package/README.md +11 -0
  27. package/create-xpine-app/README.md +20 -0
  28. package/create-xpine-app/index.js +80 -0
  29. package/create-xpine-app/package.json +30 -0
  30. package/create-xpine-app/template/README.md +50 -0
  31. package/create-xpine-app/template/eslint.config.mjs +27 -0
  32. package/create-xpine-app/template/gitignore +5 -0
  33. package/create-xpine-app/template/package.json +32 -0
  34. package/create-xpine-app/template/tsconfig.json +36 -0
  35. package/create-xpine-app/template/xpine.config.mjs +1 -0
  36. package/eslint.config.mjs +1 -1
  37. package/package.json +1 -1
@@ -26,7 +26,8 @@
26
26
  "Bash(JWT_PRIVATE_KEY=test-secret-123 node --input-type=module -e ' *)",
27
27
  "Bash(node --input-type=module -e ' *)",
28
28
  "Bash(CSRF_SECRET='csrf-secret-xyz' node /tmp/csrf-test.mjs)",
29
- "Bash(CSRF_SECRET='csrf-secret-xyz' node csrf-test.mjs)"
29
+ "Bash(CSRF_SECRET='csrf-secret-xyz' node csrf-test.mjs)",
30
+ "Bash(npm view *)"
30
31
  ]
31
32
  }
32
33
  }
@@ -0,0 +1,33 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run *)",
5
+ "Bash(PORT=8888 node ./dist/server/run/prod.js)",
6
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8888/)",
7
+ "Bash(break)",
8
+ "Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/technology/devops/my-blog-post)",
9
+ "Bash(curl -s http://localhost:8888/blog/technology/devops/my-blog-post)",
10
+ "Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/preview/some/new/post)",
11
+ "Bash(curl -s http://localhost:8888/blog/preview/some/new/post)",
12
+ "Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/totally/unknown/path)",
13
+ "Bash(curl -s http://localhost:8888/blog/totally/unknown/path)",
14
+ "Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/some-totally-random-route-xyz)",
15
+ "Bash(curl -s http://localhost:8888/some-totally-random-route-xyz)",
16
+ "Bash(curl -s http://localhost:8888/some-random-xyz)",
17
+ "Bash(curl -s -o /dev/null -w \"status=%{http_code}\\\\n\" http://localhost:8888/blog/nope)",
18
+ "Bash(curl -s http://localhost:8888/blog/nope)",
19
+ "Bash(PORT=8888 npx playwright test app-build --grep \"multi-segment\")",
20
+ "Bash(PORT=8888 npx playwright test)",
21
+ "Bash(npx eslint *)",
22
+ "Bash(echo \"=== exit: $? ===\")",
23
+ "Bash(git stash *)",
24
+ "Bash(PORT=8888 npx playwright test app-build --grep \"traverse\")",
25
+ "Bash(node -e \"console.log\\(require\\('./node_modules/jsonwebtoken/package.json'\\).version\\)\")",
26
+ "Bash(JWT_PRIVATE_KEY=test-secret-123 node --input-type=module -e ' *)",
27
+ "Bash(node --input-type=module -e ' *)",
28
+ "Bash(CSRF_SECRET='csrf-secret-xyz' node /tmp/csrf-test.mjs)",
29
+ "Bash(CSRF_SECRET='csrf-secret-xyz' node csrf-test.mjs)",
30
+ "Bash(npm view *)"
31
+ ]
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ declare module 'xpine';
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,571 @@
1
+ # XPine - Alpine.js + JSX + Tailwind framework
2
+
3
+ Combines JSX with Alpine.js for a simpler, easier development experience. Includes a static site generator.
4
+
5
+ ### Install
6
+
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 `/`
12
+
13
+ ```
14
+ import { WrapperProps } from 'xpine/dist/types';
15
+ import Base from '../components/Base';
16
+
17
+ export const config = {
18
+ data() {
19
+ return {
20
+ title: 'Home page',
21
+ description: 'The description',
22
+ }
23
+ },
24
+ wrapper({ req, children, data, routePath }: WrapperProps) {
25
+ return (
26
+ <Base
27
+ title={data?.title || 'My awesome website'}
28
+ description={data?.description}
29
+ req={req}
30
+ >
31
+ <h1>Home page wrapper</h1>
32
+ {children}
33
+ </Base>
34
+ )
35
+ },
36
+ }
37
+
38
+ export default function Home() {
39
+ return (
40
+ <div x-data="HomePageData" x-on:click="logMessage">
41
+ Hello world
42
+ </div>
43
+ );
44
+ }
45
+
46
+ <script />
47
+
48
+ export function HomePageData() {
49
+ return {
50
+ logMessage() {
51
+ console.log('Hello world');
52
+ }
53
+ };
54
+ }
55
+ ```
56
+
57
+ - src/components/Base.tsx
58
+
59
+ ```
60
+ import { JsxElement } from 'typescript';
61
+ import { ServerRequest } from 'xpine/dist/types';
62
+
63
+ type BaseProps = {
64
+ head?: JsxElement;
65
+ title: string;
66
+ description?: string;
67
+ req?: ServerRequest;
68
+ children?: JsxElement;
69
+ }
70
+
71
+ export default async function Base({
72
+ head,
73
+ title,
74
+ description,
75
+ children,
76
+ }: BaseProps) {
77
+ return (
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8" />
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
82
+ <meta name="robots" content="index,follow" />
83
+ <meta name="description" content={description || `${title} page`} />
84
+ <title>{title || 'My Website'}</title>
85
+ <link rel="stylesheet" href="/styles/global.css" />
86
+ <script defer src="/scripts/app.js"></script>
87
+ {head}
88
+ </head>
89
+ <body>
90
+ <div id="xpine-root">
91
+ {children}
92
+ </div>
93
+ </body>
94
+ </html>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ### Client side Javascript and CSS files
100
+
101
+ In the above code, we have a `/styles/global.css` file and a `/scripts/app.js` file import.
102
+
103
+ #### CSS
104
+
105
+ Create a file called `/src/public/styles/global.css` and import Tailwind:
106
+
107
+ ```
108
+ @import "tailwindcss";
109
+ ```
110
+
111
+ Note you must install tailwindcss yourself: `npm install tailwindcss`
112
+
113
+ #### Javascript
114
+
115
+ Create a file called `/src/public/scripts/index.ts`:
116
+
117
+ ```
118
+ import Alpine from 'alpinejs';
119
+
120
+ Alpine.start();
121
+ ```
122
+
123
+ Note you must install alpinejs yourself: `npm install alpinejs`
124
+
125
+ ### Dynamic routing
126
+
127
+ Create dynamic routes with paths similar to this `/src/pages/[pathA]/[pathB]`
128
+
129
+ ### Express API endpoints
130
+
131
+ 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`
132
+ ```
133
+ import { PageProps } from 'xpine/dist/types';
134
+
135
+ export default async function myEndpoint({ res }: PageProps) {
136
+ res.status(200).json({
137
+ message: 'Hello World!',
138
+ });
139
+ }
140
+ ```
141
+
142
+ ### Catch all routes
143
+ 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:
144
+ ```
145
+ export const config = {
146
+ staticPaths() {
147
+ return [
148
+ {
149
+ 0: 'hello/world',
150
+ }
151
+ ]
152
+ },
153
+ }
154
+ ```
155
+
156
+ You can get the route param in your function with req.params[0], such as how express handles catch all routes.
157
+
158
+ ### Multi-segment dynamic routes (`[...slug]`)
159
+
160
+ 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`.
161
+
162
+ 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:
163
+
164
+ ```
165
+ // /src/pages/blog/+config.ts
166
+ export default {
167
+ staticPaths() {
168
+ // Slugs can come from a CMS/database
169
+ return [
170
+ { slug: 'technology/devops/my-blog-post' },
171
+ ];
172
+ },
173
+ };
174
+ ```
175
+
176
+ ```
177
+ // /src/pages/blog/[...slug].tsx
178
+ import { PageProps } from 'xpine/dist/types';
179
+
180
+ export default function BlogPost({ req }: PageProps) {
181
+ return <div>{req.params.slug}</div>;
182
+ }
183
+ ```
184
+
185
+ 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:
186
+
187
+ ```
188
+ export default {
189
+ staticPaths() {
190
+ return [{ slug: 'technology/devops/my-blog-post' }];
191
+ },
192
+ // Resolve newly-published slugs without a rebuild
193
+ async isValid(slug, req) {
194
+ return await postExists(slug);
195
+ },
196
+ };
197
+ ```
198
+
199
+ ### Route specific middleware
200
+
201
+ 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:
202
+
203
+ ```
204
+ export const config = {
205
+ routeMiddleware(req, res, next) {
206
+ console.log('route middleware');
207
+ next();
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### CSRF protection
213
+
214
+ 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`:
215
+
216
+ ```
217
+ export default {
218
+ csrf: true,
219
+ }
220
+ ```
221
+
222
+ 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.
223
+
224
+ 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.
225
+
226
+ For `fetch`/Alpine requests, read the cookie and send it back:
227
+ ```
228
+ const token = document.cookie.split('; ').find(c => c.startsWith('csrfToken='))?.split('=')[1];
229
+ await fetch('/api/thing', {
230
+ method: 'POST',
231
+ headers: { 'x-csrf-token': decodeURIComponent(token) },
232
+ });
233
+ ```
234
+ For server-rendered HTML forms, the token is available on `res.locals.csrfToken` (pages receive `res`); render it as a hidden `<input name="_csrf">`.
235
+
236
+ You can customize the cookie/header/field names, cookie attributes, and skip specific path prefixes (e.g. signed webhooks):
237
+ ```
238
+ export default {
239
+ csrf: {
240
+ headerName: 'x-csrf-token',
241
+ cookie: { sameSite: 'strict', secure: true },
242
+ ignorePaths: ['/api/webhooks/'],
243
+ },
244
+ }
245
+ ```
246
+
247
+ ### HTML escaping (XSS protection)
248
+
249
+ Values interpolated into JSX are HTML-escaped by default, so rendering user input is safe:
250
+ ```
251
+ // req.params.slug = '<img src=x onerror=alert(1)>'
252
+ <div>{req.params.slug}</div> // renders &lt;img src=x onerror=alert(1)&gt;
253
+ ```
254
+ 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.
255
+
256
+ If you have trusted HTML that should render as-is, opt out explicitly with `raw()`:
257
+ ```
258
+ import { raw } from 'xpine';
259
+
260
+ <div>{raw(trustedHtmlString)}</div>
261
+ ```
262
+ Only use `raw()` with HTML you control — never with user input.
263
+
264
+ ### Static Site Generation
265
+
266
+ 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:
267
+ ```
268
+ export const config = {
269
+ staticPaths: true,
270
+ data() {
271
+ return {
272
+ title: 'My title'
273
+ }
274
+ }
275
+ }
276
+ ```
277
+
278
+ or a `+config.ts` file.
279
+
280
+ ### Configs
281
+
282
+ 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:
283
+ ```
284
+ export default {
285
+ staticPaths: false,
286
+ }
287
+ ```
288
+
289
+ ### Dynamic Static Pages
290
+
291
+ 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:
292
+ ```
293
+ import { ServerRequest } from 'xpine/dist/types';
294
+ import axios from 'axios';
295
+
296
+ export const config = {
297
+ staticPaths() {
298
+ return [
299
+ {
300
+ pathA: 'my-path-a2',
301
+ pathB: 'my-path-b2',
302
+ pathC: 'my-path-c2',
303
+ pathD: '2'
304
+ }
305
+ ]
306
+ },
307
+ async data(req: ServerRequest) {
308
+ const url = `https://jsonplaceholder.typicode.com/posts/${req.params.pathD}`;
309
+ try {
310
+ const { data } = await axios.get(url);
311
+ return {
312
+ ...data,
313
+ ...req.params,
314
+ };
315
+ } catch (err) {
316
+ console.error('could not fetch', url);
317
+ return {
318
+ ...req.params,
319
+ data: {},
320
+ }
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Context
327
+
328
+ Create app context, useful for things like Navbars. In `/src/context.tsx`:
329
+ ```
330
+ import { createContext } from 'xpine';
331
+
332
+ export function NavbarContext() {
333
+ const navbar = createContext([]);
334
+ return {
335
+ navbar,
336
+ }
337
+ }
338
+ ```
339
+
340
+ then in a page, say `/src/pages/about.tsx`, you can add to the NavbarContext like this:
341
+ ```
342
+ export function xpineOnLoad() {
343
+ context.addToArray('navbar', 'My awesome context 1', 2);
344
+ context.addToArray('navbar', 'My awesome context 2', new Date('January 11, 2024'));
345
+ context.addToArray('navbar', 'My awesome context 3', new Date('January 10, 2024'));
346
+ context.addToArray('navbar', 'My awesome context 4', new Date('January 30, 2024'));
347
+ }
348
+ ```
349
+
350
+ Context is sorted by array position then by date. You can then use context in your component like this:
351
+ ```
352
+ import { context } from 'xpine';
353
+
354
+ export default function Navbar() {
355
+ const navbar = context.get('navbar');
356
+ return (
357
+ <div>
358
+ {navbar.map(item => {
359
+ return <div>{item}</div>
360
+ })}
361
+ </div>
362
+ );
363
+ }
364
+ ```
365
+
366
+ ### Server set up
367
+
368
+ - src/server/app.ts
369
+
370
+ ```
371
+ import express from 'express';
372
+ import cookieParser from 'cookie-parser';
373
+ import logger from 'morgan';
374
+ import http from 'http';
375
+ import { createXPineRouter, setupEnv } from 'xpine';
376
+
377
+ await setupEnv();
378
+
379
+ export default async function startServer() {
380
+ const port = process.env.PORT || 8080;
381
+ const app = express();
382
+ app.set('view engine', 'html');
383
+
384
+ app.enable('trust proxy');
385
+ app.use(logger('dev'));
386
+ app.use(express.json());
387
+ app.use(express.urlencoded({ extended: true, }));
388
+ app.use(cookieParser());
389
+
390
+ await createXPineRouter(app);
391
+
392
+ app.set('port', port);
393
+ const server = http.createServer(app);
394
+ server.listen(port, () => {
395
+ console.info(`Server listening on port ${port}`);
396
+ });
397
+ return {
398
+ app,
399
+ server,
400
+ };
401
+ }
402
+ ```
403
+
404
+ Add a directory in `/src/server` called `run`, and create two files: `/src/server/dev.ts` and `/src/server/prod.ts`.
405
+
406
+ `dev.ts` should look like this:
407
+
408
+ ```
409
+ import { runDevServer } from 'xpine';
410
+
411
+ runDevServer();
412
+ ```
413
+
414
+ and can be run with an npm command like this in your package.json scripts: `"dev": "PORT=8888 LOCALHOST=1 xpine-dev"`.
415
+
416
+ The `prod.ts` should look like this:
417
+
418
+ ```
419
+ import startServer from '../app';
420
+
421
+ await startServer();
422
+ ```
423
+
424
+ and can be run with an npm command in your package.json scripts like this, after the app has been built with `xpine-build`:
425
+
426
+ `"start": "PORT=8888 node ./dist/server/run/prod.js"`
427
+
428
+ ### xpine.config.mjs file
429
+
430
+ Add an xpine.config.mjs file to your root directory. This is used primarily for configuring/changing file paths. Configs can be imported with `import { config } from "xpine"`.
431
+
432
+ ```
433
+ export default {}
434
+ ```
435
+
436
+ These are the following default paths:
437
+
438
+ ```
439
+ rootDir: process.cwd
440
+ srcDir: rootDir + ./src
441
+ distDir: rootDir + ./dist
442
+ packageJsonPath: rootDir + ./package.json
443
+ distPublicDir: distDir + ./public
444
+ distPublicScriptsDir: distPublicDir + ./scripts
445
+ distTempFolder: distDir + ./temp
446
+ clientJSBundlePath: distPublicScriptsDir + ./app.js
447
+ alpineDataPath: distTempFolder + ./alpine-data.ts
448
+ serverDistDir: distDir + ./server
449
+ serverDistAppPath: serverDistDir + ./app.js
450
+ pagesDir: srcDir + ./pages
451
+ distPagesDir: distDir + ./pages
452
+ publicDir: srcDir + ./public
453
+ serverDir: srcDir + ./server
454
+ runDir: serverDir + ./run
455
+ serverAppPath: serverDir + ./app.ts
456
+ globalCSSFile: publicDir + ./styles/global.css
457
+ ```
458
+
459
+ ### SPA interactivity
460
+
461
+ - data-spa="true"
462
+ - transforms link into client side URL update
463
+ - data-persistent="id"
464
+ - makes element persistent across pages that include the same persistent data-persistent tag
465
+ - data-spa-crossorigin="true"
466
+ - enables cross-origin spa navigation requests (untested)
467
+
468
+ ## API
469
+
470
+ ### Build command
471
+
472
+ `xpine-build`
473
+
474
+ ### Dev server command
475
+
476
+ `xpine-dev`
477
+
478
+ ### Auth
479
+
480
+ `import { signUser, verifyUser } from 'xpine';`
481
+
482
+ ### Config
483
+
484
+ `import { config } from 'xpine';`
485
+
486
+ ### Env
487
+
488
+ ```
489
+ import { setupEnv } from 'xpine';
490
+
491
+ await setupEnv();
492
+
493
+ ```
494
+
495
+ setupEnv also supports AWS secrets manager. Simply add SECRETS_NAME=your_aws_secret_name to your .env.{stage} file
496
+
497
+ ### Custom events
498
+ - spa-update-page-content
499
+ - sent when the page content has update
500
+ - spa-update-page-url
501
+ - sent when the page URL has update
502
+ - spa-link-click
503
+ - sent when link initially gets clicked and when link is done updating content
504
+ - the "state" value in the event detail will be "start" or "end"
505
+ - breakpoint-change
506
+ - sent when a breakpoint is changed via your Tailwind css file's breakpoints in the @theme directive, e.g.
507
+ ```
508
+ @theme {
509
+ --breakpoint-xl: 1184px;
510
+ --breakpoint-sm: 640px;
511
+ --breakpoint-md: 768px;
512
+ --breakpoint-lg: 1024px;
513
+ }
514
+ ```
515
+ - this will send a custom event with the detail being the breakpoint, such as `{ breakpoint: 'xl' }`
516
+ - note this will only send an event based on the values of the breakpoints in your @theme configuration
517
+
518
+ ### Custom scripts for certain pages
519
+
520
+ 1. Add script to src/public/scripts/pages/your_script.ts
521
+ 2. Import script into page HTML (e.g. `<script src="/scripts/pages/your_script.ts">`)
522
+ 3. To unload event listeners, use `window.addEventListener('spa-update-page-url', () => { remove event listeners here})` in the code
523
+
524
+ ### Separate client side bundles
525
+
526
+ You can create separate client side bundles in xpine.config.mjs like this:
527
+
528
+ ```
529
+ export default {
530
+ bundles: [
531
+ {
532
+ id: 'site',
533
+ excludePaths: [
534
+ // Excludes SecretPageData
535
+ '/**/pages/secret/**/*.{js,ts,tsx,jsx}',
536
+ '/**/pages/secret/*.{js,ts,tsx,jsx}',
537
+ ],
538
+ },
539
+ {
540
+ id: 'secret-page',
541
+ includePaths: [
542
+ // Only uses Alpine data coming from paths in this directory
543
+ '/**/pages/secret/**/*.{js,ts,tsx,jsx}',
544
+ ],
545
+ requireAuthentication: true, // Only authenticated users can access this .js file
546
+ }
547
+ ]
548
+ }
549
+ ```
550
+
551
+ ### Pages public scripts
552
+
553
+ If you need to add a one-off public script, add a folder in the `/public/scripts` directory called `pages`.
554
+
555
+ For example: `/public/scripts/pages/my-awesome-page/sw.js`
556
+
557
+ Which will result in the public path of `/scripts/pages/my-awesome-page/sw.js`
558
+
559
+ Any `.js` or `.ts` file in this directory will be added as-is to the `public` folder and can be used in a script tag:
560
+
561
+ ```
562
+ <script src="/scripts/pages/my-awesome-page/sw.js" />
563
+ ```
564
+
565
+ ### Standalone public files
566
+
567
+ If you need to add a file that needs direct routing, like a service worker file, add a folder in the `/public` directory called `standalone`.
568
+
569
+ For example : `/public/standalone/my-page/sw.js`
570
+
571
+ Which will result in the public path of `/my-page/sw.js`
@@ -0,0 +1,2 @@
1
+ [ ] folder specific client side JS builds
2
+ [ ] TypeError: Cannot read properties of undefined (reading 'find')
@@ -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/", "cdk.out/", "tests/dist/", "tests/playwright-report/"]
26
+ }
27
+ )
@@ -0,0 +1 @@
1
+ declare module 'xpine/jsx-runtime';