wirejs-scripts 3.0.188 → 3.0.190
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 +322 -2
- package/bin.js +129 -36
- package/package.json +3 -3
- package/ssg.js +27 -5
package/README.md
CHANGED
|
@@ -1,3 +1,323 @@
|
|
|
1
|
-
|
|
1
|
+
# wirejs-scripts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Experimental:** WireJS is an experimental hobby project and is not production software. For a professionally supported project in a similar spirit, see [AWS Blocks](https://github.com/aws-devtools-labs/aws-blocks), which benefits from lessons and ideas explored here.
|
|
4
|
+
|
|
5
|
+
Experimental build and development commands for WireJS apps.
|
|
6
|
+
|
|
7
|
+
This package owns the CLI behavior behind the generated app scripts, including TypeScript bundling, local development serving, SSR/SSG processing, hydration bundles, static asset handling, and workspace helper commands.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
Generated apps typically expose these through `package.json` scripts:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm run start # local development server + watch builds
|
|
15
|
+
npm run start:public # local development with public/NAT-PMP behavior when available
|
|
16
|
+
npm run build # production build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The underlying CLI is `wirejs-scripts`.
|
|
20
|
+
|
|
21
|
+
## Expected app layout
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
api/ API/resource workspace
|
|
25
|
+
src/ssr/ server-rendered request-time routes
|
|
26
|
+
src/ssg/ statically generated pages/artifacts
|
|
27
|
+
src/ shared source/components/layouts
|
|
28
|
+
static/ public static assets, served as /static/*
|
|
29
|
+
dist/ generated output
|
|
30
|
+
pre-dist/ intermediate build output
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## SSR handling
|
|
34
|
+
|
|
35
|
+
Files under `src/ssr/` are compiled to `dist/ssr/`. At request time, WireJS hosting matches the request path to the best compiled SSR module and invokes its exported `generate(context)` function.
|
|
36
|
+
|
|
37
|
+
A minimal request-time route can return a plain body:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/ssr/debug.txt.ts -> /debug.txt
|
|
41
|
+
import type { Context } from 'wirejs-resources';
|
|
42
|
+
|
|
43
|
+
export async function generate(context: Context) {
|
|
44
|
+
context.responseHeaders['cache-control'] = 'no-store';
|
|
45
|
+
return `Path: ${context.location.pathname}\n`;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For non-HTML SSR responses, the hosting layer infers a `content-type` from the requested extension when possible. You can also set one explicitly through `context.responseHeaders`, which is recommended when you care about charset or a more specific MIME type:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// src/ssr/feed.xml.ts -> /feed.xml
|
|
53
|
+
import type { Context } from 'wirejs-resources';
|
|
54
|
+
|
|
55
|
+
export async function generate(context: Context) {
|
|
56
|
+
context.responseHeaders['content-type'] = 'application/rss+xml; charset=utf-8';
|
|
57
|
+
context.responseHeaders['cache-control'] = 'max-age=300';
|
|
58
|
+
|
|
59
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
60
|
+
<rss version="2.0"><channel><title>Example feed</title></channel></rss>`;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// src/ssr/manifest.json.ts -> /manifest.json
|
|
66
|
+
import type { Context } from 'wirejs-resources';
|
|
67
|
+
|
|
68
|
+
export async function generate(context: Context) {
|
|
69
|
+
context.responseHeaders['content-type'] = 'application/manifest+json; charset=utf-8';
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
name: 'Example app',
|
|
72
|
+
start_url: '/',
|
|
73
|
+
display: 'standalone',
|
|
74
|
+
}, null, 2);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The examples in generated apps use `wirejs-dom/v2` for HTML. That is not required by `wirejs-scripts`, but it demonstrates the intended SSR + client-bundle flow:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/ssr/counter.ts -> /counter
|
|
82
|
+
import { html, id, text, hydrate as wireHydrate } from 'wirejs-dom/v2';
|
|
83
|
+
import type { Context } from 'wirejs-resources';
|
|
84
|
+
|
|
85
|
+
function Counter(init: { count?: number } = {}) {
|
|
86
|
+
let count = init.count ?? 0;
|
|
87
|
+
const self = html`<button id="counter" ${id('button')}>
|
|
88
|
+
Clicked ${text('count', String(count))} times
|
|
89
|
+
</button>`;
|
|
90
|
+
|
|
91
|
+
self.data.button.onclick = () => {
|
|
92
|
+
count += 1;
|
|
93
|
+
self.data.count = String(count);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function generate(context: Context) {
|
|
100
|
+
return html`<html>
|
|
101
|
+
<head><title>Counter</title></head>
|
|
102
|
+
<body>${Counter({ count: Number(context.location.searchParams.get('count') ?? 0) })}</body>
|
|
103
|
+
</html>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function hydrate() {
|
|
107
|
+
wireHydrate('counter', () => Counter());
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`generate()` is used for the server-side render. The exported `hydrate()` function is used as the browser entrypoint. This lets a module share selected components/functions between server generation and the client bundle while keeping server-only code inside `generate()` or behind server-only imports.
|
|
112
|
+
|
|
113
|
+
If `generate()` returns a DOM/document-like value, hosting serializes it as HTML. If it returns a string or other non-DOM body, hosting sends that body with a content type inferred from the requested extension when possible, unless `context.responseHeaders['content-type']` overrides it.
|
|
114
|
+
|
|
115
|
+
Routing notes:
|
|
116
|
+
|
|
117
|
+
- Request paths are matched against compiled `.js` files under `dist/ssr/`.
|
|
118
|
+
- `%` in an SSR filename acts as a wildcard pattern.
|
|
119
|
+
- Direct `/ssr` and `/ssr/*` static-file access is blocked; SSR modules are executable server code, not public assets.
|
|
120
|
+
- If SSR code changes `context.location`, hosting can emit a redirect-style response when no explicit `responseCode` is set.
|
|
121
|
+
- Pre-extension filenames work for SSR non-HTML responses: `src/ssr/feed.xml.ts` compiles to a handler for `/feed.xml`; `src/ssr/data.json.ts` handles `/data.json`.
|
|
122
|
+
|
|
123
|
+
## Client hydration bundles
|
|
124
|
+
|
|
125
|
+
SSR/SSG pages may register browser-side hydration through the DOM package used by the app. `wirejs-scripts` builds associated browser bundles for those pages so event handlers and client-only behavior can attach after generated HTML loads.
|
|
126
|
+
|
|
127
|
+
For SSR, the compiled server module lives under `dist/ssr/*.js` and the browser bundle is served through the corresponding `*.client.js` path when hydration is needed.
|
|
128
|
+
|
|
129
|
+
For SSG HTML outputs, `wirejs-scripts` probes for an exported `hydrate` path and emits a browser IIFE bundle only when appropriate. If no hydrate export exists, it emits a no-op client bundle rather than failing the whole SSG build. Non-HTML SSG outputs do not receive sibling browser JS bundles.
|
|
130
|
+
|
|
131
|
+
A useful pattern is to split code by what can safely run in both places:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// shared: safe for server generation and browser hydration
|
|
135
|
+
function TodoList() { /* returns DOM and attaches browser-safe handlers */ }
|
|
136
|
+
|
|
137
|
+
// server-only: reads request context, private resources, secrets, etc.
|
|
138
|
+
export async function generate(context) {
|
|
139
|
+
const todos = await loadTodosFor(context);
|
|
140
|
+
return Page({ todos });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// browser-only entrypoint: attaches behavior to generated markup
|
|
144
|
+
export function hydrate() {
|
|
145
|
+
wireHydrate('todos', () => TodoList());
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Because the browser bundle starts from the exported `hydrate()` entrypoint, you can avoid pulling server-only dependencies into the client by keeping them out of the functions imported/called by `hydrate()`.
|
|
150
|
+
|
|
151
|
+
## SSG handling
|
|
152
|
+
|
|
153
|
+
Files under `src/ssg/` are built and executed at build time. Each module's `generate()` output is written to `dist/` as a static artifact.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// src/ssg/about.ts -> dist/about.html
|
|
157
|
+
import { html } from 'wirejs-dom/v2';
|
|
158
|
+
|
|
159
|
+
export async function generate() {
|
|
160
|
+
return html`<html><body><h1>About</h1></body></html>`;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
SSG uses the same shared-component pattern as SSR. For static HTML with browser behavior, export both `generate()` and `hydrate()` from the page module. The server/build side calls `generate()` once, and the emitted client bundle calls `hydrate()` in the browser.
|
|
165
|
+
|
|
166
|
+
### Non-HTML SSG outputs
|
|
167
|
+
|
|
168
|
+
SSG supports "pre-extension" filenames for generated assets that are not HTML:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// src/ssg/feed.xml.ts -> dist/feed.xml
|
|
172
|
+
export async function generate() {
|
|
173
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
174
|
+
<rss version="2.0"><channel><title>Example</title></channel></rss>`;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// src/ssg/manifest.json.ts -> dist/manifest.json
|
|
180
|
+
export async function generate() {
|
|
181
|
+
return {
|
|
182
|
+
name: 'Example app',
|
|
183
|
+
start_url: '/',
|
|
184
|
+
display: 'standalone',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Convention:
|
|
190
|
+
|
|
191
|
+
- `src/ssg/page.ts` emits `dist/page.html`.
|
|
192
|
+
- `src/ssg/name.ext.ts` emits `dist/name.ext`.
|
|
193
|
+
- `.json` outputs are JSON-stringified when `generate()` returns an object.
|
|
194
|
+
- Other non-HTML outputs are stringified with `String(value)` unless `generate()` returns a string or buffer.
|
|
195
|
+
- Non-HTML outputs do not get hydration/client JS bundles.
|
|
196
|
+
|
|
197
|
+
Use SSG for pages or files that can be produced ahead of time. Use SSR for request-dependent content, per-user data, redirects, request-specific headers/cookies, or data that must be fresh on every request.
|
|
198
|
+
|
|
199
|
+
In local watch/start mode, `wirejs-scripts` watches source files and debounces SSG rebuilds. Rebuilds are queued so overlapping changes do not run multiple SSG builds concurrently.
|
|
200
|
+
|
|
201
|
+
## Static files
|
|
202
|
+
|
|
203
|
+
Put public static assets in the app's top-level `static/` directory and reference them with root-relative `/static/...` URLs, for example:
|
|
204
|
+
|
|
205
|
+
```html
|
|
206
|
+
<link rel="stylesheet" href="/static/default.css">
|
|
207
|
+
<img src="/static/images/logo.svg">
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Current behavior:
|
|
211
|
+
|
|
212
|
+
- Local development serves `/static/*` directly from the source `static/` directory.
|
|
213
|
+
- Production/build output expects static assets to be public under the `/static/` URL space.
|
|
214
|
+
- Static path resolution is normalized so requests cannot escape the intended static/dist root.
|
|
215
|
+
- Do not put private files in `static/`; it is public web content.
|
|
216
|
+
|
|
217
|
+
## API and request handling
|
|
218
|
+
|
|
219
|
+
WireJS APIs are imported like ordinary TypeScript modules. Client code, SSR, and SSG should import from the app's API package and call functions directly; `wirejs-scripts` handles the client/server wiring, request protocol, serialization, and server execution.
|
|
220
|
+
|
|
221
|
+
In short: treat the API as a module that just happens to run on the server.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// api/index.ts
|
|
225
|
+
export const todos = {
|
|
226
|
+
async list(userId: string) {
|
|
227
|
+
return [{ id: 't1', title: `Todo for ${userId}` }];
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// src/ssg or src/ssr or browser/client code
|
|
234
|
+
import { todos } from 'internal-api';
|
|
235
|
+
|
|
236
|
+
const items = await todos.list('user-1');
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The generated client module preserves end-to-end TypeScript types without requiring app code to know the wire protocol. Do not manually call WireJS's internal API transport endpoints from app code. If you need a real URL/path for callers that are not using the WireJS module protocol, define a custom `Endpoint` resource in the API package.
|
|
240
|
+
|
|
241
|
+
Custom `Endpoint` is appropriate for:
|
|
242
|
+
|
|
243
|
+
- webhooks and third-party callbacks;
|
|
244
|
+
- OIDC/provider callback paths;
|
|
245
|
+
- custom admin/settings portals;
|
|
246
|
+
- compatibility routes for callers that cannot import the WireJS API package;
|
|
247
|
+
- serving non-API content from an API module, such as generated images, files, feeds, or health-check text.
|
|
248
|
+
|
|
249
|
+
The local server creates a `wirejs-resources` `Context` for each request, then routes through static handling, SSR, API, endpoint, and not-found behavior. API modules and endpoint handlers receive the same context model used by SSR.
|
|
250
|
+
|
|
251
|
+
## Deployment config
|
|
252
|
+
|
|
253
|
+
Generated apps may include a top-level `deployment-config.ts` exporting `DeploymentConfig` from `wirejs-resources`. `wirejs-scripts` reads this file for build-time behavior, and deployment providers read the same file for hosting/infrastructure behavior.
|
|
254
|
+
|
|
255
|
+
`deployment-config.ts` is the provider-neutral place for build/deployment intent that should be supported consistently by hosting providers over time, including:
|
|
256
|
+
|
|
257
|
+
- runtime memory/timeout/Node version preferences;
|
|
258
|
+
- server bundle options such as bundled node modules, output format, and minification;
|
|
259
|
+
- extra SSG externals for modules that should remain external during static generation;
|
|
260
|
+
- branch/domain mapping;
|
|
261
|
+
- host redirects;
|
|
262
|
+
- DNS records.
|
|
263
|
+
|
|
264
|
+
Minimal build/runtime example:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { DeploymentConfig } from 'wirejs-resources';
|
|
268
|
+
|
|
269
|
+
export default {
|
|
270
|
+
runtimeDesiredMemoryMB: 1024,
|
|
271
|
+
runtimeNodeVersion: 22,
|
|
272
|
+
bundleNodeModules: ['jsdom'],
|
|
273
|
+
ssgExternalModules: ['sharp'],
|
|
274
|
+
} satisfies DeploymentConfig;
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Domain-oriented example:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { DeploymentConfig } from 'wirejs-resources';
|
|
281
|
+
|
|
282
|
+
export default {
|
|
283
|
+
domainsByBranch: {
|
|
284
|
+
main: 'staging.example.com',
|
|
285
|
+
prod: 'www.example.com',
|
|
286
|
+
'*': '{branch}.example.com',
|
|
287
|
+
},
|
|
288
|
+
redirects: [
|
|
289
|
+
{ from: 'example.com', to: 'www.example.com', mode: 'permanent' },
|
|
290
|
+
{ from: '{branch}.old.example.com', to: '{branch}.new.example.com' },
|
|
291
|
+
],
|
|
292
|
+
dnsRecordsByBranch: {
|
|
293
|
+
main: [
|
|
294
|
+
{ name: '@', zoneDomain: 'example.com', type: 'MX', values: ['1 aspmx.l.google.com.'] },
|
|
295
|
+
],
|
|
296
|
+
'*': [
|
|
297
|
+
{ name: '_verify.{branch}', zoneDomain: 'example.com', type: 'TXT', values: ['"ok-{branch}"'] },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
} satisfies DeploymentConfig;
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Provider-specific packages decide which fields they currently support and how those fields map to infrastructure. The intent is that new deployment providers should use the same `DeploymentConfig` surface whenever the concept applies.
|
|
304
|
+
|
|
305
|
+
## Implemented deployments
|
|
306
|
+
|
|
307
|
+
Currently documented deployment provider:
|
|
308
|
+
|
|
309
|
+
- [`wirejs-deploy-cdk`](../wirejs-deploy-cdk/README.md) — AWS CDK deployment with Lambda, CloudFront, S3, DynamoDB, Cognito, custom domains, redirects, and Route53 DNS records.
|
|
310
|
+
|
|
311
|
+
When working in an installed app, prefer version-locked docs from `node_modules`:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
node -e "console.log(require.resolve('wirejs-deploy-cdk/package.json').replace(/package\.json$/, 'README.md'))"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
If browsing online, use npm as a secondary reference: <https://www.npmjs.com/package/wirejs-deploy-cdk>
|
|
318
|
+
|
|
319
|
+
## Workspace helpers
|
|
320
|
+
|
|
321
|
+
Generated apps use `wirejs-scripts ws-run-parallel ...` to run matching scripts across workspaces during development.
|
|
322
|
+
|
|
323
|
+
WireJS is experimental. Expect breaking changes and prefer the README installed with your exact package version.
|
package/bin.js
CHANGED
|
@@ -300,14 +300,6 @@ async function compile(watch = false) {
|
|
|
300
300
|
const distDir = path.join(CWD, 'dist');
|
|
301
301
|
const ssrOutDir = path.join(distDir, 'ssr');
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
* Copy all files from src to dest recursively.
|
|
305
|
-
*/
|
|
306
|
-
async function copyStatic() {
|
|
307
|
-
if (!fs.existsSync(staticDir)) return;
|
|
308
|
-
await fs.promises.cp(staticDir, distDir, { recursive: true });
|
|
309
|
-
}
|
|
310
|
-
|
|
311
303
|
/**
|
|
312
304
|
* Find all .ts files recursively in a directory.
|
|
313
305
|
*/
|
|
@@ -321,6 +313,19 @@ async function compile(watch = false) {
|
|
|
321
313
|
}
|
|
322
314
|
}
|
|
323
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Resolve output extension for an SSG entrypoint.
|
|
318
|
+
*
|
|
319
|
+
* Conventions:
|
|
320
|
+
* - page.ts => page.html
|
|
321
|
+
* - page.ext.ts => page.ext
|
|
322
|
+
*/
|
|
323
|
+
function getSsgOutputExtension(ssgFile) {
|
|
324
|
+
const withoutTs = ssgFile.replace(/\.ts$/, '');
|
|
325
|
+
const explicitExt = path.extname(withoutTs).replace(/^\./, '');
|
|
326
|
+
return explicitExt || 'html';
|
|
327
|
+
}
|
|
328
|
+
|
|
324
329
|
/**
|
|
325
330
|
* Build SSG files and write the generated HTML to dist/.
|
|
326
331
|
* Also produces browser-compatible IIFE bundles at dist/*.js so that
|
|
@@ -331,15 +336,64 @@ async function compile(watch = false) {
|
|
|
331
336
|
if (ssgFiles.length === 0) return;
|
|
332
337
|
|
|
333
338
|
const preSsgDir = path.join(CWD, 'pre-dist', 'ssg');
|
|
339
|
+
const preSsgHydrateWrapperDir = path.join(CWD, 'pre-dist', '.wirejs', 'ssg-hydrate-wrappers');
|
|
334
340
|
const preDist = path.join(CWD, 'pre-dist');
|
|
335
341
|
const configuredSsgExternals = await resolveSsgExternals();
|
|
336
342
|
|
|
343
|
+
async function createSsgHydrateWrapper(ssgFile, withHydrateImport) {
|
|
344
|
+
const relativeToSsg = path.relative(ssgDir, ssgFile);
|
|
345
|
+
const wrapperPath = path.join(preSsgHydrateWrapperDir, relativeToSsg);
|
|
346
|
+
await fs.promises.mkdir(path.dirname(wrapperPath), { recursive: true });
|
|
347
|
+
|
|
348
|
+
const rawImportPath = path.relative(path.dirname(wrapperPath), ssgFile).replace(/\\/g, '/');
|
|
349
|
+
const importPath = rawImportPath.startsWith('.') ? rawImportPath : `./${rawImportPath}`;
|
|
350
|
+
const wrapperSource = withHydrateImport
|
|
351
|
+
? [
|
|
352
|
+
`import { hydrate } from ${JSON.stringify(importPath)};`,
|
|
353
|
+
'',
|
|
354
|
+
'if (typeof hydrate === "function") {',
|
|
355
|
+
'\thydrate();',
|
|
356
|
+
'}',
|
|
357
|
+
].join('\n')
|
|
358
|
+
: '// No hydrate export in source; this client bundle is intentionally a no-op.\n';
|
|
359
|
+
|
|
360
|
+
await fs.promises.writeFile(wrapperPath, wrapperSource, 'utf8');
|
|
361
|
+
return wrapperPath;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isMissingHydrateExportError(error) {
|
|
365
|
+
const messages = [error?.message ?? '', ...((error?.errors ?? []).map(e => e?.text ?? ''))]
|
|
366
|
+
.join('\n')
|
|
367
|
+
.toLowerCase();
|
|
368
|
+
return messages.includes('no matching export') && messages.includes('hydrate');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function buildSsgHydrationIife(entryPoint, browserExternal) {
|
|
372
|
+
await esbuild.build({
|
|
373
|
+
entryPoints: [entryPoint],
|
|
374
|
+
outdir: distDir,
|
|
375
|
+
outbase: preSsgHydrateWrapperDir,
|
|
376
|
+
bundle: true,
|
|
377
|
+
treeShaking: true,
|
|
378
|
+
platform: 'browser',
|
|
379
|
+
format: 'iife',
|
|
380
|
+
conditions: ['wirejs:client'],
|
|
381
|
+
external: browserExternal,
|
|
382
|
+
sourcemap: true,
|
|
383
|
+
// This path intentionally probes named hydrate exports and falls back to no-op.
|
|
384
|
+
// Keep probe failures out of stdout/stderr and handle them in catch logic.
|
|
385
|
+
logLevel: 'silent',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
337
389
|
for (const ssgFile of ssgFiles) {
|
|
338
390
|
const external = configuredSsgExternals;
|
|
339
391
|
const browserExternal = Array.from(new Set([
|
|
340
392
|
...NODE_BUILTIN_EXTERNALS,
|
|
341
393
|
...configuredSsgExternals,
|
|
342
394
|
]));
|
|
395
|
+
const outputExtension = getSsgOutputExtension(ssgFile);
|
|
396
|
+
const compiledSsgFile = path.join(preSsgDir, path.relative(ssgDir, ssgFile)).replace(/\.[^.]+$/, '.js');
|
|
343
397
|
|
|
344
398
|
// 1. Build server-side bundles (ESM, for ssg.js import()) into pre-dist/ssg/
|
|
345
399
|
await esbuild.build({
|
|
@@ -349,40 +403,43 @@ async function compile(watch = false) {
|
|
|
349
403
|
bundle: true,
|
|
350
404
|
platform: 'node',
|
|
351
405
|
format: 'esm',
|
|
406
|
+
treeShaking: true,
|
|
352
407
|
conditions: ['wirejs:client'],
|
|
353
408
|
external,
|
|
354
409
|
});
|
|
355
410
|
|
|
356
|
-
// 2. Build browser-side bundles
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
});
|
|
411
|
+
// 2. Build browser-side bundles only for HTML SSG outputs.
|
|
412
|
+
// Non-HTML artifacts should not emit a sibling .js bundle.
|
|
413
|
+
if (outputExtension === 'html') {
|
|
414
|
+
let hydrateWrapperFile = await createSsgHydrateWrapper(ssgFile, true);
|
|
415
|
+
try {
|
|
416
|
+
await buildSsgHydrationIife(hydrateWrapperFile, browserExternal);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (!isMissingHydrateExportError(error)) throw error;
|
|
419
|
+
hydrateWrapperFile = await createSsgHydrateWrapper(ssgFile, false);
|
|
420
|
+
await buildSsgHydrationIife(hydrateWrapperFile, browserExternal);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
369
423
|
}
|
|
370
424
|
|
|
371
425
|
await Promise.all(ssgFiles.map(async ssgFile => {
|
|
372
|
-
// 3. Run ssg.js on each server-side bundle to produce
|
|
426
|
+
// 3. Run ssg.js on each server-side bundle to produce final output.
|
|
373
427
|
// Pass CWD/pre-dist as SRC_DIR so ssg.js strips the correct prefix
|
|
374
428
|
// when building the hydration <script src="..."> path (e.g. /index.js).
|
|
375
429
|
const compiledSsgFile = path.join(preSsgDir, path.relative(ssgDir, ssgFile)).replace(/\.[^.]+$/, '.js');
|
|
430
|
+
const outputExtension = getSsgOutputExtension(ssgFile);
|
|
376
431
|
try {
|
|
377
|
-
const relative =
|
|
432
|
+
const relative = outputExtension === 'html'
|
|
433
|
+
? path.relative(preSsgDir, compiledSsgFile).replace(/\.js$/, '.html')
|
|
434
|
+
: path.relative(preSsgDir, compiledSsgFile).replace(/\.js$/, '');
|
|
378
435
|
console.log(`Generating ${relative}`);
|
|
379
|
-
const
|
|
380
|
-
`node ${path.join(__dirname, 'ssg.js')} ${preDist} ${compiledSsgFile}`,
|
|
436
|
+
const output = (await execPromise(
|
|
437
|
+
`node ${path.join(__dirname, 'ssg.js')} ${preDist} ${compiledSsgFile} ${outputExtension}`,
|
|
381
438
|
{ stdio: ['pipe', 'pipe', 'pipe'] }
|
|
382
439
|
)).stdout.toString();
|
|
383
|
-
const
|
|
384
|
-
await fs.promises.mkdir(path.dirname(
|
|
385
|
-
await fs.promises.writeFile(
|
|
440
|
+
const outPath = path.join(distDir, relative);
|
|
441
|
+
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
|
442
|
+
await fs.promises.writeFile(outPath, output);
|
|
386
443
|
} catch (err) {
|
|
387
444
|
logger.error(`Could not generate SSG page ${compiledSsgFile}`, err);
|
|
388
445
|
}
|
|
@@ -465,10 +522,49 @@ async function compile(watch = false) {
|
|
|
465
522
|
ssrCtx.watch();
|
|
466
523
|
}
|
|
467
524
|
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
525
|
+
// SSG watch (debounced + queued to avoid overlapping rebuilds)
|
|
526
|
+
let ssgBuildInProgress = false;
|
|
527
|
+
let ssgBuildQueued = false;
|
|
528
|
+
let ssgRebuildDebounce;
|
|
529
|
+
|
|
530
|
+
const runSsgBuild = async () => {
|
|
531
|
+
if (ssgBuildInProgress) {
|
|
532
|
+
ssgBuildQueued = true;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
ssgBuildInProgress = true;
|
|
537
|
+
try {
|
|
538
|
+
do {
|
|
539
|
+
ssgBuildQueued = false;
|
|
540
|
+
await buildSsg();
|
|
541
|
+
} while (ssgBuildQueued);
|
|
542
|
+
} finally {
|
|
543
|
+
ssgBuildInProgress = false;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
await runSsgBuild();
|
|
548
|
+
|
|
549
|
+
if (fs.existsSync(srcDir)) {
|
|
550
|
+
fs.watch(srcDir, { recursive: true }, (_eventType, filename) => {
|
|
551
|
+
if (!filename) return;
|
|
552
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
553
|
+
|
|
554
|
+
// Rebuild SSG when any source dependency changes, not just direct
|
|
555
|
+
// src/ssg/*.ts edits.
|
|
556
|
+
if (normalized.startsWith('ssr/')) return;
|
|
557
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs|json|css)$/.test(normalized)) return;
|
|
558
|
+
|
|
559
|
+
if (ssgRebuildDebounce) clearTimeout(ssgRebuildDebounce);
|
|
560
|
+
ssgRebuildDebounce = setTimeout(() => {
|
|
561
|
+
ssgRebuildDebounce = undefined;
|
|
562
|
+
runSsgBuild().catch(error => {
|
|
563
|
+
logger.error('SSG rebuild failed', error);
|
|
564
|
+
});
|
|
565
|
+
}, 200);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
472
568
|
|
|
473
569
|
// NAT-PMP / --public flag
|
|
474
570
|
if (actionArgs.includes('--public') || actionArgs.includes('-p')) {
|
|
@@ -498,6 +594,7 @@ async function compile(watch = false) {
|
|
|
498
594
|
const server = wirejsCreateServer({
|
|
499
595
|
apiDir: path.join(CWD, 'api'),
|
|
500
596
|
distDir,
|
|
597
|
+
staticDir,
|
|
501
598
|
port: httpPort,
|
|
502
599
|
attributes: () => [
|
|
503
600
|
new SystemAttribute('wirejs', 'deployment-type', {
|
|
@@ -573,10 +670,6 @@ async function compile(watch = false) {
|
|
|
573
670
|
});
|
|
574
671
|
}
|
|
575
672
|
|
|
576
|
-
// Static files
|
|
577
|
-
console.log('copying static files');
|
|
578
|
-
await copyStatic();
|
|
579
|
-
|
|
580
673
|
// SSG
|
|
581
674
|
console.log('copying ssg files');
|
|
582
675
|
await buildSsg();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-scripts",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.190",
|
|
4
4
|
"description": "Basic build and start commands for wirejs apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node-cron": "^4.2.1",
|
|
29
29
|
"rimraf": "^6.0.1",
|
|
30
30
|
"wirejs-dom": "^1.0.44",
|
|
31
|
-
"wirejs-hosting": "^0.1.
|
|
32
|
-
"wirejs-resources": "^0.1.
|
|
31
|
+
"wirejs-hosting": "^0.1.192",
|
|
32
|
+
"wirejs-resources": "^0.1.192"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/ssg.js
CHANGED
|
@@ -4,15 +4,37 @@ import { useJSDOM } from 'wirejs-dom/v2';
|
|
|
4
4
|
|
|
5
5
|
const SRC_DIR = process.argv[2];
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function serializeNonHtml(value, outputExtension) {
|
|
8
|
+
if (value === undefined || value === null) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
if (Buffer.isBuffer(value)) {
|
|
12
|
+
return value.toString('utf8');
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
if (outputExtension === 'json') {
|
|
18
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function build(filename, outputExtension = 'html') {
|
|
8
24
|
useJSDOM(JSDOM);
|
|
9
25
|
|
|
10
26
|
try {
|
|
11
27
|
const module = await import(filename);
|
|
12
28
|
|
|
13
29
|
if (typeof module.generate === 'function') {
|
|
14
|
-
const
|
|
15
|
-
|
|
30
|
+
const generated = await module.generate(filename);
|
|
31
|
+
|
|
32
|
+
if (outputExtension !== 'html') {
|
|
33
|
+
return serializeNonHtml(generated, outputExtension);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const doc = generated;
|
|
37
|
+
const doctype = doc?.parentNode?.doctype?.name || '';
|
|
16
38
|
|
|
17
39
|
let hydrationsFound = 0;
|
|
18
40
|
while (globalThis.pendingDehydrations?.length > 0) {
|
|
@@ -31,7 +53,7 @@ async function build(filename) {
|
|
|
31
53
|
doc.outerHTML
|
|
32
54
|
].join('');
|
|
33
55
|
} else {
|
|
34
|
-
return;
|
|
56
|
+
return '';
|
|
35
57
|
}
|
|
36
58
|
} catch (err) {
|
|
37
59
|
console.error(`Could not generate page ${filename}`, err);
|
|
@@ -39,4 +61,4 @@ async function build(filename) {
|
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
console.log(await build(process.argv[3]));
|
|
64
|
+
console.log(await build(process.argv[3], process.argv[4] || 'html'));
|