poveste 0.5.2 → 0.5.3

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/node/bin.js CHANGED
@@ -6,6 +6,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  const { version } = JSON.parse(fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8'));
7
7
  process.env.NODE_ENV = 'development';
8
8
  process.env.HISTOIRE = 'true';
9
+ process.env.POVESTE = 'true';
9
10
  const program = sade('poveste');
10
11
  program.version(version);
11
12
  program.command('dev')
@@ -1 +1 @@
1
- {"version":3,"file":"pinceau-tokens.d.ts","sourceRoot":"","sources":["../../../src/node/builtin-plugins/pinceau-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAA;AAI5D,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,QAAQ,GAAE,oBAAyB,GAAG,MAAM,CA2DzE"}
1
+ {"version":3,"file":"pinceau-tokens.d.ts","sourceRoot":"","sources":["../../../src/node/builtin-plugins/pinceau-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAA;AAI5D,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,QAAQ,GAAE,oBAAyB,GAAG,MAAM,CA6DzE"}
@@ -41,7 +41,8 @@ export function pinceauTokens(_options = {}) {
41
41
  });
42
42
  }
43
43
  },
44
- onDev(api, onCleanup) {
44
+ async onDev(api, onCleanup) {
45
+ await generate(api);
45
46
  const watcher = api.watcher.watch(themePath)
46
47
  .on('change', () => generate(api))
47
48
  .on('add', () => generate(api));
@@ -1 +1 @@
1
- {"version":3,"file":"tailwind-tokens.d.ts","sourceRoot":"","sources":["../../../src/node/builtin-plugins/tailwind-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAA;AAI5D,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAqBD,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAgE1E"}
1
+ {"version":3,"file":"tailwind-tokens.d.ts","sourceRoot":"","sources":["../../../src/node/builtin-plugins/tailwind-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAA;AAI5D,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAqBD,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAkE1E"}
@@ -61,8 +61,9 @@ export function tailwindTokens(options = {}) {
61
61
  }
62
62
  }
63
63
  },
64
- onDev(api, onCleanup) {
64
+ async onDev(api, onCleanup) {
65
65
  if (tailwindCssFile) {
66
+ await generate(api);
66
67
  const watcher = api.watcher.watch(tailwindCssFile)
67
68
  .on('change', () => generate(api))
68
69
  .on('add', () => generate(api));
@@ -1,5 +1,5 @@
1
1
  export interface DevOptions {
2
- port: number;
2
+ port?: number | string;
3
3
  open?: boolean;
4
4
  host?: string | boolean;
5
5
  config?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/node/commands/dev.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,iBAoDnD"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/node/commands/dev.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,iBAsDnD"}
@@ -3,7 +3,9 @@ import pc from 'picocolors';
3
3
  import { resolveConfigFile } from '../config.js';
4
4
  import { createContext } from '../context.js';
5
5
  import { createServer } from '../server.js';
6
+ import { resolvePort } from './port.js';
6
7
  export async function devCommand(options) {
8
+ const port = resolvePort(options.port, 'dev');
7
9
  let stop;
8
10
  async function start() {
9
11
  const ctx = await createContext({
@@ -11,7 +13,7 @@ export async function devCommand(options) {
11
13
  mode: 'dev',
12
14
  });
13
15
  const { server, viteConfigFile, close } = await createServer(ctx, {
14
- port: options.port,
16
+ port,
15
17
  open: options.open,
16
18
  host: options.host,
17
19
  });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reads a `--port` value off the parsed CLI options.
3
+ *
4
+ * sade declares the flag as taking a value but does not enforce one, so
5
+ * `poveste preview --port` arrives as the boolean `true` and reaches node's
6
+ * listen validation, which rejects it with `ERR_INVALID_ARG_VALUE` and names
7
+ * neither the flag nor the mistake. A non-numeric value fails the same way.
8
+ */
9
+ export declare function resolvePort(value: unknown, command: string): number | undefined;
10
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../../src/node/commands/port.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoB/E"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Reads a `--port` value off the parsed CLI options.
3
+ *
4
+ * sade declares the flag as taking a value but does not enforce one, so
5
+ * `poveste preview --port` arrives as the boolean `true` and reaches node's
6
+ * listen validation, which rejects it with `ERR_INVALID_ARG_VALUE` and names
7
+ * neither the flag nor the mistake. A non-numeric value fails the same way.
8
+ */
9
+ export function resolvePort(value, command) {
10
+ if (value === undefined || value === null) {
11
+ return undefined;
12
+ }
13
+ // Checked by type, not by coercion: a valueless flag arrives as `true`, and
14
+ // `Number(true)` is 1, a valid port that would bind to a privileged one.
15
+ const port = typeof value === 'number'
16
+ ? value
17
+ : typeof value === 'string' && value.trim() !== ''
18
+ ? Number(value)
19
+ : Number.NaN;
20
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
21
+ throw new TypeError(`--port needs a number between 0 and 65535, got ${JSON.stringify(value)}. Try \`poveste ${command} --port 6006\`.`);
22
+ }
23
+ return port;
24
+ }
@@ -1,5 +1,5 @@
1
1
  export interface PreviewOptions {
2
- port?: number;
2
+ port?: number | string;
3
3
  host?: string | boolean;
4
4
  open?: boolean;
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/node/commands/preview.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,iBAiB3D"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/node/commands/preview.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,iBAmB3D"}
@@ -1,6 +1,8 @@
1
1
  import { createContext } from '../context.js';
2
2
  import { startPreview } from '../preview.js';
3
+ import { resolvePort } from './port.js';
3
4
  export async function previewCommand(options) {
5
+ const port = resolvePort(options.port, 'preview');
4
6
  const ctx = await createContext({
5
7
  mode: 'build',
6
8
  });
@@ -10,7 +12,7 @@ export async function previewCommand(options) {
10
12
  }
11
13
  }
12
14
  const { printUrls } = await startPreview({
13
- port: options.port,
15
+ port,
14
16
  host: options.host,
15
17
  open: options.open,
16
18
  }, ctx);
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/node/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,aAAa,EAEd,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAkB3C,wBAAgB,gBAAgB,IAAI,aAAa,CA4HhD;AAED,eAAO,MAAM,eAAe,UAU3B,CAAA;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ1F;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAexF;AAED,eAAO,MAAM,gBAAgB,uBAK3B,CAAA;AAEF,eAAO,MAAM,WAAW,uBA8DtB,CAAA;AAEF,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAsB,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB7H;AA2BD,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,iBAoE/C;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,0BAE1D;AAED,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,UAAU;QAClB;;WAEG;QACH,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;QAChC;;WAEG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;KAClC;CACF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/node/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EAEV,aAAa,EAEd,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAkB3C,wBAAgB,gBAAgB,IAAI,aAAa,CA2HhD;AAED,eAAO,MAAM,eAAe,UAU3B,CAAA;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ1F;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAexF;AAED,eAAO,MAAM,gBAAgB,uBAK3B,CAAA;AAEF,eAAO,MAAM,WAAW,uBA8DtB,CAAA;AAEF,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAsB,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB7H;AA2BD,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,iBAoE/C;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,0BAE1D;AAED,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,UAAU;QAClB;;WAEG;QACH,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;QAChC;;WAEG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;KAClC;CACF"}
@@ -110,7 +110,6 @@ export function getDefaultConfig() {
110
110
  contrastColor: '#eee',
111
111
  },
112
112
  ],
113
- sandboxDarkClass: 'dark',
114
113
  routerMode: 'history',
115
114
  build: {
116
115
  excludeFromVendorsChunk: [],
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/node/preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAG3C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAwBrG"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/node/preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAG3C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BrG"}
@@ -12,7 +12,12 @@ export async function startPreview(options, ctx) {
12
12
  port,
13
13
  host: options.host,
14
14
  open: options.open,
15
- strictPort: false,
15
+ // A port asked for by name has to be that port or nothing. Moving to the
16
+ // next free one serves the book where nobody is looking, and lands it on
17
+ // whatever example owns that number, whose suite then adopts it (#175).
18
+ // Without an explicit port there is no such expectation, and `build`'s
19
+ // internal preview reads the port back off the server.
20
+ strictPort: options.port != null,
16
21
  },
17
22
  });
18
23
  const baseUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}${ctx.resolvedViteConfig.base}`;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAa3C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACxB;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,mBAAwB;;;;GAsNjF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAa3C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACxB;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,mBAAwB;;;;GA6NjF"}
@@ -19,6 +19,12 @@ export async function createServer(ctx, options = {}) {
19
19
  if (options.host) {
20
20
  viteConfig.server.host = options.host;
21
21
  }
22
+ // Same rule as `preview`: a port asked for by name is that port or
23
+ // nothing. Floating to the next free one hands back a server nobody asked
24
+ // for, and whoever opens the port they typed reads a different book.
25
+ if (options.port != null) {
26
+ viteConfig.server.strictPort = true;
27
+ }
22
28
  }
23
29
  const server = await createViteServer(mergeViteConfig(viteConfig, {
24
30
  optimizeDeps: { include: viteConfig.optimizeDeps?.include ?? [], noDiscovery: collecting },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.5.2",
4
+ "version": "0.5.3",
5
5
  "description": "A fast story book powered by Vite",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -68,10 +68,10 @@
68
68
  "sirv": "^3.0.0",
69
69
  "unhead": "^3.1.0",
70
70
  "vite-node": "^6.0.0",
71
- "@poveste/app": "^0.5.2",
72
- "@poveste/controls": "^0.5.2",
73
- "@poveste/shared": "^0.5.2",
74
- "@poveste/vendors": "^0.5.2"
71
+ "@poveste/app": "^0.5.3",
72
+ "@poveste/shared": "^0.5.3",
73
+ "@poveste/controls": "^0.5.3",
74
+ "@poveste/vendors": "^0.5.3"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/fs-extra": "^11.0.4",
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolvePort } from '../commands/port.js'
3
+
4
+ describe('resolvePort', () => {
5
+ it('returns undefined when the flag was not passed', () => {
6
+ expect(resolvePort(undefined, 'preview')).toBeUndefined()
7
+ })
8
+
9
+ it('parses the number sade hands over as a string', () => {
10
+ expect(resolvePort('4569', 'preview')).toBe(4569)
11
+ })
12
+
13
+ it('passes a number through', () => {
14
+ expect(resolvePort(6006, 'dev')).toBe(6006)
15
+ })
16
+
17
+ // sade declares `--port <port>` but does not enforce the value, so a bare
18
+ // `--port` arrives as `true`. `Number(true)` is 1 — a valid port that binds
19
+ // to a privileged one — so this has to be rejected by type.
20
+ it.each([
21
+ ['a valueless flag', true],
22
+ ['a non-numeric value', 'abc'],
23
+ ['an empty value', ''],
24
+ ['a port above the range', '99999'],
25
+ ['a fractional port', '80.5'],
26
+ ])('rejects %s', (_name, value) => {
27
+ expect(() => resolvePort(value, 'preview')).toThrow(/--port needs a number/)
28
+ })
29
+
30
+ it('names the command in the message, so the hint is runnable', () => {
31
+ expect(() => resolvePort(true, 'dev')).toThrow(/poveste dev --port 6006/)
32
+ })
33
+ })
package/src/node/bin.ts CHANGED
@@ -9,6 +9,7 @@ const { version } = JSON.parse(fs.readFileSync(resolve(__dirname, '../../package
9
9
 
10
10
  process.env.NODE_ENV = 'development'
11
11
  process.env.HISTOIRE = 'true'
12
+ process.env.POVESTE = 'true'
12
13
 
13
14
  const program = sade('poveste')
14
15
  program.version(version)
@@ -52,7 +52,9 @@ export function pinceauTokens(_options: PinceauTokensOptions = {}): Plugin {
52
52
  }
53
53
  },
54
54
 
55
- onDev(api, onCleanup) {
55
+ async onDev(api, onCleanup) {
56
+ await generate(api)
57
+
56
58
  const watcher = api.watcher.watch(themePath)
57
59
  .on('change', () => generate(api))
58
60
  .on('add', () => generate(api))
@@ -77,8 +77,10 @@ export function tailwindTokens(options: TailwindTokensOptions = {}): Plugin {
77
77
  }
78
78
  },
79
79
 
80
- onDev(api, onCleanup) {
80
+ async onDev(api, onCleanup) {
81
81
  if (tailwindCssFile) {
82
+ await generate(api)
83
+
82
84
  const watcher = api.watcher.watch(tailwindCssFile)
83
85
  .on('change', () => generate(api))
84
86
  .on('add', () => generate(api))
@@ -4,15 +4,18 @@ import pc from 'picocolors'
4
4
  import { resolveConfigFile } from '../config.js'
5
5
  import { createContext } from '../context.js'
6
6
  import { createServer } from '../server.js'
7
+ import { resolvePort } from './port.js'
7
8
 
8
9
  export interface DevOptions {
9
- port: number
10
+ port?: number | string
10
11
  open?: boolean
11
12
  host?: string | boolean
12
13
  config?: string
13
14
  }
14
15
 
15
16
  export async function devCommand(options: DevOptions) {
17
+ const port = resolvePort(options.port, 'dev')
18
+
16
19
  let stop: () => Promise<void>
17
20
 
18
21
  async function start() {
@@ -21,7 +24,7 @@ export async function devCommand(options: DevOptions) {
21
24
  mode: 'dev',
22
25
  })
23
26
  const { server, viteConfigFile, close } = await createServer(ctx, {
24
- port: options.port,
27
+ port,
25
28
  open: options.open,
26
29
  host: options.host,
27
30
  })
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reads a `--port` value off the parsed CLI options.
3
+ *
4
+ * sade declares the flag as taking a value but does not enforce one, so
5
+ * `poveste preview --port` arrives as the boolean `true` and reaches node's
6
+ * listen validation, which rejects it with `ERR_INVALID_ARG_VALUE` and names
7
+ * neither the flag nor the mistake. A non-numeric value fails the same way.
8
+ */
9
+ export function resolvePort(value: unknown, command: string): number | undefined {
10
+ if (value === undefined || value === null) {
11
+ return undefined
12
+ }
13
+
14
+ // Checked by type, not by coercion: a valueless flag arrives as `true`, and
15
+ // `Number(true)` is 1, a valid port that would bind to a privileged one.
16
+ const port = typeof value === 'number'
17
+ ? value
18
+ : typeof value === 'string' && value.trim() !== ''
19
+ ? Number(value)
20
+ : Number.NaN
21
+
22
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
23
+ throw new TypeError(
24
+ `--port needs a number between 0 and 65535, got ${JSON.stringify(value)}. Try \`poveste ${command} --port 6006\`.`,
25
+ )
26
+ }
27
+
28
+ return port
29
+ }
@@ -1,13 +1,16 @@
1
1
  import { createContext } from '../context.js'
2
2
  import { startPreview } from '../preview.js'
3
+ import { resolvePort } from './port.js'
3
4
 
4
5
  export interface PreviewOptions {
5
- port?: number
6
+ port?: number | string
6
7
  host?: string | boolean
7
8
  open?: boolean
8
9
  }
9
10
 
10
11
  export async function previewCommand(options: PreviewOptions) {
12
+ const port = resolvePort(options.port, 'preview')
13
+
11
14
  const ctx = await createContext({
12
15
  mode: 'build',
13
16
  })
@@ -19,7 +22,7 @@ export async function previewCommand(options: PreviewOptions) {
19
22
  }
20
23
 
21
24
  const { printUrls } = await startPreview({
22
- port: options.port,
25
+ port,
23
26
  host: options.host,
24
27
  open: options.open,
25
28
  }, ctx)
@@ -122,7 +122,6 @@ export function getDefaultConfig(): PovesteConfig {
122
122
  contrastColor: '#eee',
123
123
  },
124
124
  ],
125
- sandboxDarkClass: 'dark',
126
125
  routerMode: 'history',
127
126
  build: {
128
127
  excludeFromVendorsChunk: [],
@@ -26,7 +26,12 @@ export async function startPreview(options: StartPreviewOptions, ctx: Context):
26
26
  port,
27
27
  host: options.host,
28
28
  open: options.open,
29
- strictPort: false,
29
+ // A port asked for by name has to be that port or nothing. Moving to the
30
+ // next free one serves the book where nobody is looking, and lands it on
31
+ // whatever example owns that number, whose suite then adopts it (#175).
32
+ // Without an explicit port there is no such expectation, and `build`'s
33
+ // internal preview reads the port back off the server.
34
+ strictPort: options.port != null,
30
35
  },
31
36
  })
32
37
 
@@ -30,6 +30,13 @@ export async function createServer(ctx: Context, options: CreateServerOptions =
30
30
  if (options.host) {
31
31
  viteConfig.server.host = options.host
32
32
  }
33
+
34
+ // Same rule as `preview`: a port asked for by name is that port or
35
+ // nothing. Floating to the next free one hands back a server nobody asked
36
+ // for, and whoever opens the port they typed reads a different book.
37
+ if (options.port != null) {
38
+ viteConfig.server.strictPort = true
39
+ }
33
40
  }
34
41
 
35
42
  const server = await createViteServer(