datastar-ssegen 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }