datastar-ssegen 0.0.1

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.
@@ -0,0 +1,24 @@
1
+ # publish to npm
2
+ name: Publish Package
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ release:
8
+ types:
9
+ - created
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v3
16
+ - name: Setup Node.js environment
17
+ uses: actions/setup-node@v3
18
+ with:
19
+ node-version: '18'
20
+ registry-url: 'https://registry.npmjs.org/'
21
+ - name: Publish to npm
22
+ run: npm publish
23
+ env:
24
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ ![Version](https://img.shields.io/github/package-json/v/jmcudd/datastar-ssegen?filename=package.json)
2
+ ![Stars](https://img.shields.io/github/stars/jmcudd/datastar-ssegen?style=flat)
3
+
4
+ # datastar-ssegen
5
+
6
+ ## Overview
7
+
8
+ The `datastar-ssegen` is a backend JavaScript module designed to generate Server-Sent Events (SSE) for connected [Datastar](https://data-star.dev/) clients. It supports popular server frameworks such as Express.js, Node.js, and Hyper Express.js.
9
+
10
+ This package is engineered to integrate tightly with request and response objects of these backend frameworks, enabling efficient and reactive web application development.
11
+
12
+ ### Key Features
13
+
14
+ - Real-time updates with Server-Sent Events tailored for Datastar clients
15
+ - Seamless integration with Express.js, Hyper Express.js, and Node HTTP
16
+
17
+ ### Installation
18
+
19
+ Install the package via npm:
20
+
21
+ ```bash
22
+ npm install datastar-ssegen
23
+ ```
24
+
25
+ ### Quick Start Example with Express.js
26
+
27
+ Here's a straightforward example of setting up an Express.js server with the datastar-ssegen:
28
+
29
+ ```javascript
30
+ import express from 'express';
31
+ import { ServerSentEventGenerator } from 'datastar-ssegen';
32
+
33
+ const app = express();
34
+ app.use(express.json());
35
+
36
+ // Define event handlers here
37
+
38
+ app.get('/messages', handleMessages);
39
+ app.get('/clock', handleClock);
40
+
41
+ const PORT = 3101;
42
+ app.listen(PORT, () => {
43
+ console.log(`Server running at http://localhost:${PORT}`);
44
+ });
45
+ ```
46
+
47
+ ### Client Interaction Example
48
+
49
+ Here's a simple HTML page to interact with the server:
50
+
51
+ ```html
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
57
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
+ <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js"></script>
59
+ <title>SSE Example</title>
60
+ </head>
61
+ <body>
62
+ <h1>SSE Demo</h1>
63
+ <div id="greeting-area">Greeting: <button onclick="sse('/messages')">Get Greeting</button></div>
64
+ <div id="clock-area">Current Time: <button onclick="sse('/clock')">Start Clock</button></div>
65
+ </body>
66
+ </html>
67
+ ```
68
+
69
+ ### Available Functions
70
+
71
+ The `ServerSentEventGenerator` provides several functions to facilitate communication with connected Datastar clients using Server-Sent Events:
72
+
73
+ - **`init(request, response)`**: Initializes SSE communication with the specified request and response.
74
+
75
+ - **`_send(eventType, dataLines, sendOptions)`**: Sends a server-sent event (SSE) to the client. Options include setting an `eventId` and defining `retryDuration`.
76
+
77
+ - **`ReadSignals(signals)`**: Reads and merges signals based on HTTP methods with predefined signals, useful for parsing query or body data sent to the server.
78
+
79
+ - **`MergeFragments(fragments, options)`**: Sends a merge fragments event to update HTML content on the client. Options include `selector`, `mergeMode`, `settleDuration`, and `useViewTransition`.
80
+
81
+ - **`RemoveFragments(selector, options)`**: Dispatches events to remove HTML elements based on a CSS selector. Options can set a `settleDuration` or `useViewTransition`.
82
+
83
+ - **`MergeSignals(signals, options)`**: Sends a merge signals event to update or add client-side signals. Options may include `onlyIfMissing`.
84
+
85
+ - **`RemoveSignals(paths, options)`**: Sends an event to remove specific client-side signals identified by paths.
86
+
87
+ - **`ExecuteScript(script, options)`**: Directs the client to execute specified JavaScript code. Options can enable `autoRemove` of the script after execution.
88
+
89
+ This expanded set provides comprehensive functionality to build interactive web applications with real-time updates and dynamic HTML and signal management.
@@ -0,0 +1,91 @@
1
+ import { ServerSentEventGenerator } from "../index.js";
2
+
3
+ export let backendStore = {
4
+ someBackendValue: "This is something",
5
+ };
6
+ export const homepage = (name = "") => {
7
+ return `<html>
8
+ <head>
9
+ <title>${name} Datastar Test</title>
10
+ <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@develop/bundles/datastar.js"></script>
11
+ </head>
12
+ <body>
13
+ <h3>${name} Datastar Test</h3>
14
+ <div data-signals="{theme: 'light', lastUpdate:'Never', xyz:'some signal'}">
15
+ <h3>Long Lived SSE:</h3>
16
+ <div id="clock" data-on-load="sse('/clock')">...Loading Clock</div>
17
+
18
+ <h3>MergeSignals:</h3>
19
+ <div>Last User Interaction:<span data-text="lastUpdate.value"></span></div>
20
+ <h3>Merge Fragments</h3>
21
+ <div id="quote">No Quote</div>
22
+ <button data-on-click="sse('/quote')">MergeFragments</button>
23
+ <h3>RemoveFragments</h3>
24
+ <div id="trash">
25
+ Remove me please!
26
+ <button data-on-click="sse('/removeTrash')">RemoveFragments</button>
27
+ </div>
28
+ <h3>ExecuteScript</h3>
29
+ <div>Print to Console</div>
30
+ <button data-on-click="sse('/printToConsole')">ExecuteScript</button>
31
+
32
+ <h3>ReadSignals</h3>
33
+ <button data-on-click="sse('/readSignals')">ReadSignals</button>
34
+
35
+ <h3>ReadSignals (post)</h3>
36
+ <button data-on-click="sse('/readSignals', {method: 'post'})">ReadSignals (post)</button>
37
+
38
+
39
+ <h3>RemoveSignals</h3>
40
+ <div>Signal xyz:<span data-text="xyz.value"></span></div>
41
+ <button data-on-click="sse('/removeSignal')">Test RemoveSignals: xyz</button>
42
+ </div>
43
+ </body>
44
+ </html>`;
45
+ };
46
+
47
+ export const handleQuote = async (req, res) => {
48
+ const sse = ServerSentEventGenerator.init(req, res);
49
+ const qoutes = [
50
+ "Any app that can be written in JavaScript, will eventually be written in JavaScript. - Jeff Atwood",
51
+ "JavaScript is the world's most misunderstood programming language. - Douglas Crockford",
52
+ "The strength of JavaScript is that you can do anything. The weakness is that you will. - Reg Braithwaite",
53
+ ];
54
+ const randomQuote = (arr) => arr[Math.floor(Math.random() * arr.length)];
55
+ await sse.MergeFragments(`<div id="quote">${randomQuote(qoutes)}</div>`);
56
+ await sse.MergeSignals({ lastUpdate: Date.now() });
57
+ res.end();
58
+ };
59
+
60
+ export const handleReadSignals = async (req, res) => {
61
+ const sse = ServerSentEventGenerator.init(req, res);
62
+ backendStore = await sse.ReadSignals(backendStore);
63
+ console.log("backendStore updated", backendStore);
64
+ res.end();
65
+ };
66
+
67
+ export const handleRemoveTrash = async (req, res) => {
68
+ const sse = ServerSentEventGenerator.init(req, res);
69
+ await sse.RemoveFragments("#trash");
70
+ await sse.MergeSignals({ lastUpdate: Date.now() });
71
+ res.end();
72
+ };
73
+
74
+ export const handleExecuteScript = async (req, res) => {
75
+ const sse = ServerSentEventGenerator.init(req, res);
76
+ await sse.ExecuteScript(`console.log("Hello from the backend!")`);
77
+ await sse.MergeSignals({ lastUpdate: Date.now() });
78
+ res.end();
79
+ };
80
+
81
+ export const handleClock = async function (req, res) {
82
+ const sse = ServerSentEventGenerator.init(req, res);
83
+ setInterval(async () => {
84
+ await sse.MergeFragments(`<div id="clock">${new Date()}</div>`);
85
+ }, 1000);
86
+ };
87
+
88
+ export const handleRemoveSignal = async (req, res) => {
89
+ const sse = ServerSentEventGenerator.init(req, res);
90
+ await sse.RemoveSignals(["xyz"]);
91
+ };
@@ -0,0 +1,38 @@
1
+ import express from "express";
2
+ import { ServerSentEventGenerator } from "../index.js";
3
+
4
+ import {
5
+ homepage,
6
+ handleQuote,
7
+ handleReadSignals,
8
+ handleRemoveTrash,
9
+ handleExecuteScript,
10
+ handleClock,
11
+ handleRemoveSignal,
12
+ } from "./commonHandlers.js";
13
+
14
+ const app = express();
15
+ const PORT = 3101;
16
+
17
+ // Middleware to parse incoming JSON bodies if needed
18
+ app.use(express.json());
19
+
20
+ app.get("/", (req, res) => {
21
+ res.send(homepage("Express.js"));
22
+ });
23
+
24
+ app.get("/quote", handleQuote);
25
+
26
+ app.get("/readSignals", handleReadSignals);
27
+
28
+ app.get("/removeTrash", handleRemoveTrash);
29
+
30
+ app.get("/printToConsole", handleExecuteScript);
31
+
32
+ app.get("/clock", handleClock);
33
+
34
+ app.get("/removeSignal", handleRemoveSignal);
35
+
36
+ app.listen(PORT, () => {
37
+ console.log(`Express.js server http://localhost:${PORT}`);
38
+ });
@@ -0,0 +1,42 @@
1
+ import HyperExpress from "hyper-express";
2
+
3
+ import {
4
+ homepage,
5
+ handleQuote,
6
+ handleReadSignals,
7
+ handleRemoveTrash,
8
+ handleExecuteScript,
9
+ handleClock,
10
+ handleRemoveSignal,
11
+ } from "./commonHandlers.js";
12
+
13
+ // Initialize HyperExpress server
14
+ const server = new HyperExpress.Server();
15
+ const PORT = 3102;
16
+
17
+ // Configure routes
18
+ server.get("/", (req, res) => {
19
+ res.html(homepage("hyper-express"));
20
+ });
21
+
22
+ server.get("/quote", handleQuote);
23
+
24
+ server.get("/readSignals", handleReadSignals);
25
+
26
+ server.get("/removeTrash", handleRemoveTrash);
27
+
28
+ server.get("/printToConsole", handleExecuteScript);
29
+
30
+ server.get("/clock", handleClock);
31
+
32
+ server.get("/removeSignal", handleRemoveSignal);
33
+
34
+ // Start the server
35
+ server
36
+ .listen(PORT)
37
+ .then(() => {
38
+ console.log(`HyperExpress server http://localhost:${PORT}`);
39
+ })
40
+ .catch((error) => {
41
+ console.error("Error starting server:", error);
42
+ });
@@ -0,0 +1,71 @@
1
+ import http from "http";
2
+ import {
3
+ homepage,
4
+ handleQuote,
5
+ handleReadSignals,
6
+ handleRemoveTrash,
7
+ handleExecuteScript,
8
+ handleClock,
9
+ handleRemoveSignal,
10
+ } from "./commonHandlers.js";
11
+
12
+ import url from "url";
13
+
14
+ // Initialize an HTTP server
15
+ const PORT = 3100;
16
+
17
+ const server = http.createServer((req, res) => {
18
+ if (req.method === "GET") {
19
+ const parsedUrl = url.parse(req.url);
20
+ switch (parsedUrl.pathname) {
21
+ case "/":
22
+ res.writeHead(200, { "Content-Type": "text/html" });
23
+ res.end(homepage("node.js"));
24
+ break;
25
+
26
+ case "/quote":
27
+ handleQuote(req, res);
28
+ break;
29
+
30
+ case "/readSignals":
31
+ handleReadSignals(req, res);
32
+ break;
33
+
34
+ case "/removeTrash":
35
+ handleRemoveTrash(req, res);
36
+ break;
37
+
38
+ case "/printToConsole":
39
+ handleExecuteScript(req, res);
40
+ break;
41
+
42
+ case "/clock":
43
+ handleClock(req, res);
44
+ break;
45
+
46
+ case "/removeSignal":
47
+ handleRemoveSignal(req, res);
48
+ break;
49
+
50
+ default:
51
+ res.writeHead(404, { "Content-Type": "text/plain" });
52
+ res.end("404 Not Found");
53
+ }
54
+ } else if (req.method === "POST") {
55
+ const parsedUrl = url.parse(req.url);
56
+ switch (parsedUrl.pathname) {
57
+ case "/readSignals":
58
+ console.log("post readsignals");
59
+ handleReadSignals(req, res);
60
+ break;
61
+ }
62
+ } else {
63
+ res.writeHead(405, { "Content-Type": "text/plain" });
64
+ res.end("Method Not Allowed");
65
+ }
66
+ });
67
+
68
+ // Start the server
69
+ server.listen(PORT, () => {
70
+ console.log(`Node.js server http://localhost:${PORT}`);
71
+ });
package/index.js ADDED
@@ -0,0 +1,273 @@
1
+ import url from "url";
2
+ import querystring from "querystring";
3
+
4
+ /**
5
+ * ServerSentEventGenerator is responsible for initializing and handling
6
+ * server-sent events (SSE) for different web frameworks.
7
+ */
8
+ export const ServerSentEventGenerator = {
9
+ /**
10
+ * Initializes the server-sent event generator.
11
+ *
12
+ * @param {object} res - The response object from the framework.
13
+ * @returns {Object} Methods for manipulating server-sent events.
14
+ */
15
+ init: function (request, response) {
16
+ return {
17
+ headersSent: false,
18
+ req: request,
19
+ res: response,
20
+ /**
21
+ * @typedef {Object} SendOptions
22
+ * @property {?string} [eventId=null] - Event ID to attach.
23
+ * @property {number} [retryDuration=1000] - Retry duration in milliseconds.
24
+ */
25
+
26
+ /**
27
+ * Sends a server-sent event (SSE) to the client.
28
+ *
29
+ * @param {string} eventType - The type of the event.
30
+ * @param {string[]} dataLines - Lines of data to send.
31
+ * @param {SendOptions} [sendOptions] - Additional options for sending events.
32
+ */
33
+ _send: function (
34
+ eventType,
35
+ dataLines,
36
+ sendOptions = {
37
+ eventId: null,
38
+ retryDuration: 1000,
39
+ }
40
+ ) {
41
+ //Prepare the message for sending.
42
+ let data = dataLines.map((line) => `data: ${line}\n`).join("") + "\n";
43
+ let eventString = "";
44
+ if (sendOptions.eventId != null) {
45
+ eventString += `id: ${sendOptions.eventId}\n`;
46
+ }
47
+ if (eventType) {
48
+ eventString += `event: ${eventType}\n`;
49
+ }
50
+ eventString += `retry: ${sendOptions.retryDuration}\n`;
51
+ eventString += data;
52
+
53
+ //Send Event
54
+ if (!this.headersSent) {
55
+ this.res.setHeader("Cache-Control", "nocache");
56
+ this.res.setHeader("Connection", "keep-alive");
57
+ this.res.setHeader("Content-Type", "text/event-stream");
58
+ this.headersSent = true;
59
+ }
60
+ this.res.write(eventString);
61
+
62
+ return eventString;
63
+ },
64
+
65
+ /**
66
+ * Reads signals based on HTTP methods and merges them with provided signals.
67
+ *
68
+ * @param {object} signals - Predefined signals to merge with.
69
+ * @returns {Promise<object>} Merged signals object.
70
+ */
71
+ ReadSignals: async function (signals) {
72
+ if (this.req.method === "GET") {
73
+ // Parse the URL
74
+ const parsedUrl = url.parse(this.req.url);
75
+ const parsedQuery = querystring.parse(parsedUrl.query);
76
+ const datastarParam = parsedQuery.datastar;
77
+
78
+ const query = JSON.parse(datastarParam);
79
+ return {
80
+ ...signals,
81
+ ...query,
82
+ };
83
+ } else {
84
+ const body = await new Promise((resolve, reject) => {
85
+ let chunks = "";
86
+ this.req.on("data", (chunk) => {
87
+ chunks += chunk;
88
+ });
89
+ this.req.on("end", () => {
90
+ console.log("No more data in response.");
91
+ resolve(chunks);
92
+ });
93
+ });
94
+ let parsedBody = {};
95
+ try {
96
+ parsedBody = JSON.parse(body);
97
+ } catch (err) {
98
+ console.error(
99
+ "Problem reading signals, could not parse body as JSON."
100
+ );
101
+ }
102
+ console.log("parsed Body", parsedBody);
103
+ return { ...signals, ...parsedBody };
104
+ }
105
+ },
106
+ /**
107
+ * @typedef {Object} MergeFragmentsOptions
108
+ * @property {string} [selector] - CSS selector affected.
109
+ * @property {string} [mergeMode="morph"] - Mode for merging.
110
+ * @property {number} [settleDuration=300] - Duration to settle.
111
+ * @property {?boolean} [useViewTransition=null] - Use view transition.
112
+ * @property {?string} [eventId=null] - Event ID to attach.
113
+ * @property {?number} [retryDuration=null] - Retry duration in milliseconds.
114
+ */
115
+
116
+ /**
117
+ * Sends a merge fragments event.
118
+ *
119
+ * @param {string[]} fragments - Array of fragment identifiers.
120
+ * @param {MergeFragmentsOptions} options - Additional options for merging.
121
+ * @throws Will throw an error if fragments are missing.
122
+ */
123
+ MergeFragments: function (
124
+ fragments,
125
+ options = {
126
+ selector: null,
127
+ mergeMode: "morph",
128
+ settleDuration: 300,
129
+ useViewTransition: null,
130
+ eventId: null,
131
+ retryDuration: null,
132
+ }
133
+ ) {
134
+ let dataLines = [];
135
+ if (options?.selector != null)
136
+ dataLines.push(`selector ${options.selector}`);
137
+ if (options?.settleDuration != null)
138
+ dataLines.push(`settleDuration ${options.settleDuration}`);
139
+ if (options?.useViewTransition != null)
140
+ dataLines.push(`useViewTransition ${options.useViewTransition}`);
141
+ if (fragments) {
142
+ if (typeof fragments === "string") {
143
+ // Handle case where 'fragments' is a string
144
+ dataLines.push(`fragments ${fragments.replace(/[\r\n]+/g, "")}`);
145
+ } else if (Array.isArray(fragments)) {
146
+ // Handle case where 'fragments' is an array
147
+ fragments.forEach((frag) => {
148
+ dataLines.push(`fragments ${frag.replace(/[\r\n]+/g, "")}`);
149
+ });
150
+ } else {
151
+ throw Error(
152
+ "Invalid type for fragments. Expected string or array."
153
+ );
154
+ }
155
+ } else {
156
+ throw Error("MergeFragments missing fragment(s).");
157
+ }
158
+ return this._send("datastar-merge-fragments", dataLines, {
159
+ eventId: options?.eventId,
160
+ retryDuration: options?.retryDuration,
161
+ });
162
+ },
163
+ /**
164
+ * @typedef {Object} RemoveFragmentsOptions
165
+ * @property {number} [settleDuration] - Duration to settle.
166
+ * @property {?boolean} [useViewTransition=null] - Use view transition.
167
+ * @property {?string} [eventId=null] - Event ID to attach.
168
+ * @property {?number} [retryDuration=null] - Retry duration in milliseconds.
169
+ */
170
+
171
+ /**
172
+ * Sends a remove fragments event.
173
+ *
174
+ * @param {string} selector - CSS selector of fragments to remove.
175
+ * @param {RemoveFragmentsOptions} options - Additional options for removing.
176
+ * @throws Will throw an error if selector is missing.
177
+ */
178
+ RemoveFragments: function (selector, options) {
179
+ let dataLines = [];
180
+ if (selector) {
181
+ dataLines.push(`selector ${selector}`);
182
+ } else {
183
+ throw Error("RemoveFragments missing selector.");
184
+ }
185
+ if (options?.settleDuration != null)
186
+ dataLines.push(`settleDuration ${options.settleDuration}`);
187
+ if (options?.useViewTransition != null)
188
+ dataLines.push(`useViewTransition ${options.useViewTransition}`);
189
+ return this._send(`datastar-remove-fragments`, dataLines, {
190
+ eventId: options?.eventId,
191
+ retryDuration: options?.retryDuration,
192
+ });
193
+ },
194
+ /**
195
+ * @typedef {Object} MergeSignalsOptions
196
+ * @property {boolean} [onlyIfMissing] - Merge only if signals are missing.
197
+ * @property {?string} [eventId=null] - Event ID to attach.
198
+ * @property {?number} [retryDuration=null] - Retry duration in milliseconds.
199
+ */
200
+
201
+ /**
202
+ * Sends a merge signals event.
203
+ *
204
+ * @param {object} signals - Signals to merge.
205
+ * @param {MergeSignalsOptions} options - Additional options for merging.
206
+ * @throws Will throw an error if signals are missing.
207
+ */
208
+ MergeSignals: function (signals, options) {
209
+ let dataLines = [];
210
+ if (options?.onlyIfMissing === true) {
211
+ dataLines.push(`onlyIfMissing true`);
212
+ }
213
+ if (signals) {
214
+ dataLines.push(`signals ${JSON.stringify(signals)}`);
215
+ } else {
216
+ throw Error("MergeSignals missing signals.");
217
+ }
218
+ return this._send(`datastar-merge-signals`, dataLines, {
219
+ eventId: options?.eventId,
220
+ retryDuration: options?.retryDuration,
221
+ });
222
+ },
223
+ /**
224
+ * Sends a remove signals event.
225
+ *
226
+ * @param {string[]} paths - Paths of signals to remove.
227
+ * @param {SendOptions} options - Additional options for removing signals.
228
+ * @throws Will throw an error if paths are missing.
229
+ */
230
+ RemoveSignals: function (paths, options) {
231
+ let dataLines = [];
232
+ if (paths) {
233
+ paths
234
+ .map((path) => {
235
+ dataLines.push(`paths ${path}`);
236
+ })
237
+ .join("");
238
+ } else {
239
+ throw Error("RemoveSignals missing paths");
240
+ }
241
+ return this._send(`datastar-remove-signals`, dataLines, {
242
+ eventId: options?.eventId,
243
+ retryDuration: options?.retryDuration,
244
+ });
245
+ },
246
+ /**
247
+ * @typedef {Object} ExecuteScriptOptions
248
+ * @property {boolean} [autoRemove] - Automatically remove the script after execution.
249
+ * @property {?string} [eventId=null] - Event ID to attach.
250
+ * @property {?number} [retryDuration=null] - Retry duration in milliseconds.
251
+ */
252
+
253
+ /**
254
+ * Executes a script on the client-side.
255
+ *
256
+ * @param {string} script - Script code to execute.
257
+ * @param {ExecuteScriptOptions} options - Additional options for execution.
258
+ */
259
+ ExecuteScript: function (script, options) {
260
+ let dataLines = [];
261
+ if (options?.autoRemove != null)
262
+ dataLines.push(`autoRemove ${options.autoRemove}`);
263
+ if (script) {
264
+ dataLines.push(`script ${script}`);
265
+ }
266
+ return this._send(`datastar-execute-script`, dataLines, {
267
+ eventId: options?.eventId,
268
+ retryDuration: options?.retryDuration,
269
+ });
270
+ },
271
+ };
272
+ },
273
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "datastar-ssegen",
3
+ "version": "0.0.1",
4
+ "description": "Datastar Server-Sent Event generator",
5
+ "author": "John Cudd",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "scripts": {
9
+ "dev": "concurrently -c \"red,green,blue\" -n \"node,express,hyper-express\" \"npm run node\" \"npm run express\" \"npm run hyper-express\"",
10
+ "node": "nodemon examples/node.example.js",
11
+ "express": "nodemon examples/express.example.js",
12
+ "hyper-express": "nodemon examples/hyper-express.example.js"
13
+ },
14
+ "keywords": ["datastar", "hypermedia", "sse"],
15
+ "license": "mit",
16
+ "homepage": "https://github.com/jmcudd/datastar-ssegen#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/jmcudd/datastar-ssegen.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/jmcudd/datastar-ssegen/issues"
23
+ },
24
+ "devdependencies": {
25
+ "concurrently": "^9.1.0",
26
+ "express": "^4.21.2",
27
+ "hyper-express": "^6.17.3",
28
+ "nodemon": "^3.1.9",
29
+ "npm-run-all": "^4.1.5"
30
+ }
31
+ }