blogwright 0.4.0-beta.4 → 0.4.0-beta.5

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/dist/init.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /*
2
2
  * `blogwright init` - the first-run wizard. Asks the four core questions a
3
- * new site needs, then each already-discovered plugin's own `init(io)`
3
+ * new site needs, whether it is server-rendered (and with which framework,
4
+ * which picks its `ssr` block and entry shim), then each already-discovered
5
+ * plugin's own `init(io)`
4
6
  * questions (in deterministic, name-sorted order), and writes ONE commented
5
7
  * config/production.jsonc carrying every answered block. Runs before any
6
8
  * context exists (there is no config to load yet), so it takes its ports
@@ -9,7 +11,7 @@
9
11
  * plugins as a plain array, never a `ModuleLoader`, so this stays a domain
10
12
  * module.
11
13
  */
12
- import { colors, findRepoRoot, parseConfig, } from 'blogwright-core';
14
+ import { colors, findRepoRoot, parseConfig, WEB_FUNCTION_REGIONS, } from 'blogwright-core';
13
15
  import { renderConfigBlock } from './config-block.js';
14
16
  const SITE_NAME_PATTERN = /^[a-z0-9-]+$/;
15
17
  const GITHUB_REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/;
@@ -113,6 +115,114 @@ async function collectPluginBlocks(plugins, terminal, logger) {
113
115
  }
114
116
  return blocks;
115
117
  }
118
+ const SSR_GUIDE_URL = 'https://blogwright.iamstan.dev/guides/server-rendered-sites/';
119
+ /**
120
+ * The `ssr` block per framework, matching the guide's recipes: where the build
121
+ * puts the server, the variables that server reads its listen address from
122
+ * (so Blogwright generates the entry shim), and the browser assets kept on S3.
123
+ * `other` writes the defaults for the operator to fill in.
124
+ */
125
+ const SSR_FRAMEWORKS = {
126
+ astro: {
127
+ label: 'Astro (@astrojs/node standalone)',
128
+ anchor: 'astro-node-standalone',
129
+ entries: [
130
+ { prop: '"packageDir": "dist"', comment: 'all of dist/: the adapter looks for server/' },
131
+ { prop: '"entryPoint": "server/entry.mjs"' },
132
+ {
133
+ prop: '"shim": { "host": "HOST", "port": "PORT" }',
134
+ comment: 'generated entry shim: the server reads HOST and PORT',
135
+ },
136
+ { prop: '"staticDir": "dist/client"' },
137
+ { prop: '"staticPaths": ["/_astro/*", "/favicon.svg", "/robots.txt"]' },
138
+ ],
139
+ },
140
+ sveltekit: {
141
+ label: 'SvelteKit (adapter-node)',
142
+ anchor: 'sveltekit-adapter-node',
143
+ entries: [
144
+ { prop: '"packageDir": "build"' },
145
+ { prop: '"entryPoint": "index.js"' },
146
+ {
147
+ prop: '"shim": { "host": "HOST", "port": "PORT" }',
148
+ comment: 'generated entry shim: the server reads HOST and PORT',
149
+ },
150
+ { prop: '"staticDir": "build/client"' },
151
+ { prop: '"staticPaths": ["/_app/*", "/favicon.png"]' },
152
+ ],
153
+ },
154
+ nuxt: {
155
+ label: 'Nuxt (Nitro node-server)',
156
+ anchor: 'nuxt-nitro-node-server-preset',
157
+ entries: [
158
+ { prop: '"packageDir": ".output/server"' },
159
+ { prop: '"entryPoint": "index.mjs"' },
160
+ {
161
+ prop: '"shim": { "host": "NITRO_HOST", "port": "NITRO_PORT" }',
162
+ comment: 'generated entry shim: Nitro reads NITRO_HOST and NITRO_PORT',
163
+ },
164
+ { prop: '"staticDir": ".output/public"' },
165
+ { prop: '"staticPaths": ["/_nuxt/*", "/favicon.ico"]' },
166
+ ],
167
+ },
168
+ 'tanstack-start': {
169
+ label: 'TanStack Start (Nitro node-server)',
170
+ anchor: 'tanstack-start-nitro',
171
+ entries: [
172
+ { prop: '"packageDir": ".output/server"' },
173
+ { prop: '"entryPoint": "index.mjs"' },
174
+ {
175
+ prop: '"shim": { "host": "NITRO_HOST", "port": "NITRO_PORT" }',
176
+ comment: 'generated entry shim: Nitro reads NITRO_HOST and NITRO_PORT',
177
+ },
178
+ { prop: '"staticDir": ".output/public"' },
179
+ { prop: '"staticPaths": ["/assets/*", "/favicon.svg"]' },
180
+ ],
181
+ },
182
+ nextjs: {
183
+ label: 'Next.js (standalone output)',
184
+ anchor: 'nextjs-standalone-output',
185
+ entries: [
186
+ { prop: '"packageDir": ".next/standalone"', comment: 'Next serves its own static assets' },
187
+ { prop: '"entryPoint": "server.js"' },
188
+ {
189
+ prop: '"shim": { "host": "HOSTNAME", "port": "PORT" }',
190
+ comment: 'generated entry shim: the server reads HOSTNAME and PORT',
191
+ },
192
+ ],
193
+ },
194
+ other: {
195
+ label: 'your framework',
196
+ anchor: 'the-entry-shim',
197
+ entries: [
198
+ { prop: '"packageDir": "dist"', comment: 'the built server output, repo-relative' },
199
+ {
200
+ prop: '"entryPoint": "index.js"',
201
+ comment: 'starts the server; add "shim" unless it listens on AWS_LAMBDA_HTTP_ENDPOINT',
202
+ },
203
+ ],
204
+ },
205
+ };
206
+ /**
207
+ * Ask whether the site is server-rendered and, if so, which framework, and
208
+ * return that framework's recipe; `undefined` for a static site.
209
+ */
210
+ async function askSsrFramework(terminal, logger) {
211
+ const wanted = await ask(terminal, logger, {
212
+ prompt: 'server-rendered (SSR) app on a Lambda Web Function? (y/n)',
213
+ defaultValue: 'n',
214
+ validate: (v) => (/^(y|yes|n|no)$/i.test(v) ? undefined : 'answer y or n'),
215
+ });
216
+ if (!/^y/i.test(wanted ?? ''))
217
+ return undefined;
218
+ const names = Object.keys(SSR_FRAMEWORKS);
219
+ const choice = await ask(terminal, logger, {
220
+ prompt: `framework (${names.join(', ')})`,
221
+ required: true,
222
+ validate: (v) => Object.hasOwn(SSR_FRAMEWORKS, v.toLowerCase()) ? undefined : `expected ${names.join(', ')}`,
223
+ });
224
+ return SSR_FRAMEWORKS[choice.toLowerCase()];
225
+ }
116
226
  /** Render one top-level item with its own trailing comma, or none when it is last. */
117
227
  function renderTopLevelItem(item, last) {
118
228
  const comma = last ? '' : ',';
@@ -121,13 +231,14 @@ function renderTopLevelItem(item, last) {
121
231
  return ` ${item.prop}${comma}${item.comment ? ` // ${item.comment}` : ''}`;
122
232
  }
123
233
  /**
124
- * Render the whole `config/production.jsonc` body: the four core entries in
125
- * their fixed, commented style, followed by every plugin block already
234
+ * Render the whole `config/production.jsonc` body: the core entries in
235
+ * their fixed, commented style, then the `ssr` block when the site is
236
+ * server-rendered, followed by every plugin block already
126
237
  * rendered by `collectPluginBlocks` - each its own top-level property, comma
127
238
  * discipline shared with the core entries via `renderTopLevelItem` so no
128
- * block ever leaves (or follows) a stray comma. `pluginBlocks` empty
129
- * reproduces exactly the entries-only output this wizard has always
130
- * written - byte for byte, pinned by `init.test.ts`.
239
+ * block ever leaves (or follows) a stray comma. With no `ssr` and
240
+ * `pluginBlocks` empty this reproduces exactly the entries-only output this
241
+ * wizard has always written - byte for byte, pinned by `init.test.ts`.
131
242
  */
132
243
  function renderConfig(opts) {
133
244
  const items = [
@@ -145,6 +256,8 @@ function renderConfig(opts) {
145
256
  comment: 'enables the GitHub OIDC deploy role',
146
257
  });
147
258
  }
259
+ if (opts.ssr)
260
+ items.push({ block: renderConfigBlock('ssr', opts.ssr.entries) });
148
261
  for (const block of opts.pluginBlocks)
149
262
  items.push({ block });
150
263
  const body = items.map((item, i) => renderTopLevelItem(item, i === items.length - 1));
@@ -172,13 +285,13 @@ export async function initSite(fs, terminal, logger, plugins, root) {
172
285
  logger.error(`${configPath} already exists - edit it directly, or pass --config elsewhere`);
173
286
  return 1;
174
287
  }
175
- logger.info(colors.bold('Welcome to blogwright - four questions and you are live.'));
288
+ logger.info(colors.bold('Welcome to blogwright - a few questions and you are live.'));
176
289
  const siteName = await ask(terminal, logger, {
177
290
  prompt: 'site name (lowercase slug, names every AWS resource)',
178
291
  required: true,
179
292
  validate: (v) => SITE_NAME_PATTERN.test(v) ? undefined : 'must be lowercase letters, digits, or dashes',
180
293
  });
181
- const region = await ask(terminal, logger, {
294
+ let region = await ask(terminal, logger, {
182
295
  prompt: 'AWS region',
183
296
  defaultValue: 'us-east-1',
184
297
  required: true,
@@ -190,12 +303,24 @@ export async function initSite(fs, terminal, logger, plugins, root) {
190
303
  prompt: 'GitHub repo for CI deploys, owner/repo (blank to skip)',
191
304
  validate: (v) => (GITHUB_REPO_PATTERN.test(v) ? undefined : 'expected owner/repo'),
192
305
  });
306
+ const ssr = await askSsrFramework(terminal, logger);
307
+ if (ssr && !WEB_FUNCTION_REGIONS.includes(region)) {
308
+ const regions = WEB_FUNCTION_REGIONS.join(', ');
309
+ logger.warn(`server-rendered sites run on Lambda Web Functions, available in ${regions}`);
310
+ region = await ask(terminal, logger, {
311
+ prompt: 'AWS region',
312
+ defaultValue: 'us-east-1',
313
+ required: true,
314
+ validate: (v) => (WEB_FUNCTION_REGIONS.includes(v) ? undefined : `expected ${regions}`),
315
+ });
316
+ }
193
317
  const pluginBlocks = await collectPluginBlocks(plugins, terminal, logger);
194
318
  const rendered = renderConfig({
195
319
  region: region,
196
320
  siteName: siteName,
197
321
  domain,
198
322
  githubRepo,
323
+ ssr,
199
324
  pluginBlocks,
200
325
  });
201
326
  // Re-parsed before it is trusted onto disk, mirroring `plugin-commands.ts`'s
@@ -208,6 +333,8 @@ export async function initSite(fs, terminal, logger, plugins, root) {
208
333
  logger.ok(`wrote ${configPath}`);
209
334
  logger.info('');
210
335
  logger.info(colors.bold('Next steps:'));
336
+ if (ssr)
337
+ logger.step(`set up the build for ${ssr.label}: ${SSR_GUIDE_URL}#${ssr.anchor}`);
211
338
  logger.step(`blogwright bootstrap${domain ? '' : ' # add --domain later if you get one'}`);
212
339
  logger.step('blogwright deploy');
213
340
  logger.info(colors.dim(' (bootstrap prints ACM validation CNAMEs when a domain is set)'));
package/dist/nodes.d.ts CHANGED
@@ -22,6 +22,14 @@ export declare function builderImageAction(image: {
22
22
  * is how build-agent changes propagate through CI without a separate `bootstrap`.
23
23
  */
24
24
  export declare function reconcileBuilderImage(ctx: OpsContext): Promise<void>;
25
+ /** Where the CLI-owned placeholder package lives, inside the Lambda-read prefix. */
26
+ export declare const PLACEHOLDER_ARTIFACT_KEY = "web/artifacts/placeholder.zip";
27
+ /**
28
+ * The placeholder the Web Function serves until the first deploy: a tiny
29
+ * CLI-owned server that listens on `AWS_LAMBDA_HTTP_ENDPOINT` and answers 503.
30
+ * Zipped with a fixed timestamp so identical bytes never churn.
31
+ */
32
+ export declare function placeholderPackageZip(): Uint8Array;
25
33
  /**
26
34
  * The workflow's OIDC subject claim, scoped per environment to match how each one
27
35
  * deploys: previews from any PR ref; staging from pushes to main; production from the