polen 0.10.0-next.19 → 0.10.0-next.21

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.
Files changed (79) hide show
  1. package/README.md +2 -2
  2. package/build/api/api.d.ts +1 -0
  3. package/build/api/api.d.ts.map +1 -1
  4. package/build/api/api.js +1 -0
  5. package/build/api/api.js.map +1 -1
  6. package/build/api/builder/builder.d.ts +8 -2
  7. package/build/api/builder/builder.d.ts.map +1 -1
  8. package/build/api/builder/builder.js +6 -20
  9. package/build/api/builder/builder.js.map +1 -1
  10. package/build/api/config/configurator.d.ts +31 -8
  11. package/build/api/config/configurator.d.ts.map +1 -1
  12. package/build/api/config/configurator.js +9 -10
  13. package/build/api/config/configurator.js.map +1 -1
  14. package/build/api/config/merge.d.ts.map +1 -1
  15. package/build/api/config/merge.js +14 -21
  16. package/build/api/config/merge.js.map +1 -1
  17. package/build/api/content/scan.js +1 -1
  18. package/build/api/content/sidebar.js +4 -4
  19. package/build/api/project/index.d.ts +2 -0
  20. package/build/api/project/index.d.ts.map +1 -0
  21. package/build/api/project/index.js +2 -0
  22. package/build/api/project/index.js.map +1 -0
  23. package/build/api/project/validate.d.ts +11 -0
  24. package/build/api/project/validate.d.ts.map +1 -0
  25. package/build/api/project/validate.js +30 -0
  26. package/build/api/project/validate.js.map +1 -0
  27. package/build/api/vite/plugins/build.js +1 -1
  28. package/build/api/vite/plugins/core.d.ts.map +1 -1
  29. package/build/api/vite/plugins/core.js +1 -0
  30. package/build/api/vite/plugins/core.js.map +1 -1
  31. package/build/api/vite/plugins/serve.js +1 -1
  32. package/build/api/vite/plugins/serve.js.map +1 -1
  33. package/build/cli/_/parameters.d.ts +6 -0
  34. package/build/cli/_/parameters.d.ts.map +1 -0
  35. package/build/cli/_/parameters.js +9 -0
  36. package/build/cli/_/parameters.js.map +1 -0
  37. package/build/cli/commands/build.js +17 -10
  38. package/build/cli/commands/build.js.map +1 -1
  39. package/build/cli/commands/config/create.js +12 -1
  40. package/build/cli/commands/config/create.js.map +1 -1
  41. package/build/cli/commands/create.js +3 -5
  42. package/build/cli/commands/create.js.map +1 -1
  43. package/build/cli/commands/dev.js +14 -4
  44. package/build/cli/commands/dev.js.map +1 -1
  45. package/build/lib/graphql-document/components/GraphQLDocument.d.ts.map +1 -1
  46. package/build/lib/graphql-document/components/GraphQLDocument.js +3 -1
  47. package/build/lib/graphql-document/components/GraphQLDocument.js.map +1 -1
  48. package/build/lib/kit-temp.d.ts +33 -0
  49. package/build/lib/kit-temp.d.ts.map +1 -1
  50. package/build/lib/kit-temp.js +48 -0
  51. package/build/lib/kit-temp.js.map +1 -1
  52. package/build/lib/react-router-aid/get-paths-patterns.js +1 -1
  53. package/build/project-data.d.ts +1 -0
  54. package/build/project-data.d.ts.map +1 -1
  55. package/build/template/server/main.js +2 -1
  56. package/build/template/server/main.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/api/api.ts +1 -0
  59. package/src/api/builder/builder.ts +13 -23
  60. package/src/api/config/configurator.ts +41 -19
  61. package/src/api/config/merge.ts +18 -21
  62. package/src/api/content/scan.ts +1 -1
  63. package/src/api/content/sidebar.ts +4 -4
  64. package/src/api/project/index.ts +1 -0
  65. package/src/api/project/validate.ts +41 -0
  66. package/src/api/vite/plugins/build.ts +1 -1
  67. package/src/api/vite/plugins/core.ts +1 -0
  68. package/src/api/vite/plugins/serve.ts +1 -1
  69. package/src/cli/_/parameters.ts +10 -0
  70. package/src/cli/commands/build.ts +25 -10
  71. package/src/cli/commands/config/create.ts +18 -1
  72. package/src/cli/commands/create.ts +4 -4
  73. package/src/cli/commands/dev.ts +19 -5
  74. package/src/lib/graphql-document/components/GraphQLDocument.tsx +8 -6
  75. package/src/lib/kit-temp.test.ts +85 -1
  76. package/src/lib/kit-temp.ts +51 -0
  77. package/src/lib/react-router-aid/get-paths-patterns.ts +1 -1
  78. package/src/project-data.ts +1 -0
  79. package/src/template/server/main.ts +2 -1
@@ -1,4 +1,5 @@
1
1
  import { Vite } from '#dep/vite/index'
2
+ import { spreadShallow } from '#lib/kit-temp'
2
3
  import type { ConfigInput } from './configurator.js'
3
4
 
4
5
  /**
@@ -13,40 +14,36 @@ export const mergeInputs = (
13
14
  return base
14
15
  }
15
16
 
16
- const merged: ConfigInput = {
17
- ...base,
18
- ...overrides,
19
- }
17
+ const merged: ConfigInput = spreadShallow(base, overrides)
20
18
 
21
19
  // Merge schema if both have it
22
- if (base.schema || overrides.schema) {
20
+ if (base.schema ?? overrides.schema) {
23
21
  merged.schema = overrides.schema ?? base.schema
24
22
  }
25
23
 
26
24
  // Merge build config
27
- if (base.build || overrides.build) {
28
- merged.build = {
29
- ...base.build,
30
- ...overrides.build,
31
- }
25
+ if (base.build ?? overrides.build) {
26
+ merged.build = spreadShallow(base.build, overrides.build)
27
+ }
28
+
29
+ // Merge server config
30
+ if (base.server ?? overrides.server) {
31
+ merged.server = spreadShallow(base.server, overrides.server)
32
32
  }
33
33
 
34
34
  // Merge advanced config
35
- if (base.advanced || overrides.advanced) {
36
- merged.advanced = {
37
- ...base.advanced,
38
- ...overrides.advanced,
39
- }
35
+ if (base.advanced ?? overrides.advanced) {
36
+ merged.advanced = spreadShallow(base.advanced, overrides.advanced)
40
37
 
41
38
  // Merge advanced.watch config
42
- if (base.advanced?.watch || overrides.advanced?.watch) {
43
- merged.advanced.watch = {
44
- ...base.advanced?.watch,
45
- ...overrides.advanced?.watch,
46
- }
39
+ if (base.advanced?.watch ?? overrides.advanced?.watch) {
40
+ merged.advanced.watch = spreadShallow(
41
+ base.advanced?.watch,
42
+ overrides.advanced?.watch,
43
+ )
47
44
 
48
45
  // Merge watch.also arrays
49
- if (base.advanced?.watch?.also || overrides.advanced?.watch?.also) {
46
+ if (base.advanced?.watch?.also ?? overrides.advanced?.watch?.also) {
50
47
  merged.advanced.watch.also = [
51
48
  ...(base.advanced?.watch?.also ?? []),
52
49
  ...(overrides.advanced?.watch?.also ?? []),
@@ -73,7 +73,7 @@ export const scan = async (options: {
73
73
  // Update pages to have virtual root as parent if they don't have one
74
74
  const pagesWithVirtualRoot = pagesWithIds.map(page => ({
75
75
  ...page,
76
- parentId: page.parentId || `__virtual_root__`,
76
+ parentId: page.parentId ?? `__virtual_root__`,
77
77
  }))
78
78
 
79
79
  tree = Tree.fromList([virtualRoot, ...pagesWithVirtualRoot])
@@ -173,13 +173,13 @@ const buildSidebarForDirectory = (topLevelDir: string, pages: Page[]): Sidebar =
173
173
  }
174
174
 
175
175
  // Process top-level pages (direct children of the directory)
176
- const topLevelPages = pagesByParent.get(topLevelDir) || []
176
+ const topLevelPages = pagesByParent.get(topLevelDir) ?? []
177
177
 
178
178
  // Sort pages by their directory order (extracted from file path)
179
179
  const sortedTopLevelPages = [...topLevelPages].sort((a, b) => {
180
180
  // For sections, we need to look at the directory name in the file path
181
- const dirA = a.route.file.path.relative.dir.split(`/`).pop() || ``
182
- const dirB = b.route.file.path.relative.dir.split(`/`).pop() || ``
181
+ const dirA = a.route.file.path.relative.dir.split(`/`).pop() ?? ``
182
+ const dirB = b.route.file.path.relative.dir.split(`/`).pop() ?? ``
183
183
 
184
184
  // Extract order from directory names like "10_b", "20_c"
185
185
  const orderMatchA = /^(\d+)[_-]/.exec(dirA)
@@ -197,7 +197,7 @@ const buildSidebarForDirectory = (topLevelDir: string, pages: Page[]): Sidebar =
197
197
  for (const page of sortedTopLevelPages) {
198
198
  const pageName = page.route.logical.path[page.route.logical.path.length - 1]!
199
199
  const childPath = page.route.logical.path.join(`/`)
200
- const childPages = pagesByParent.get(childPath) || []
200
+ const childPages = pagesByParent.get(childPath) ?? []
201
201
 
202
202
  if (childPages.length > 0 || FileRouter.routeIsFromIndexFile(page.route)) {
203
203
  // This is a section (has children or is an index page for a subdirectory)
@@ -0,0 +1 @@
1
+ export * from './validate.js'
@@ -0,0 +1,41 @@
1
+ import { Fs } from '@wollybeard/kit'
2
+ import consola from 'consola'
3
+
4
+ export interface ValidateProjectOptions {
5
+ mustExist?: boolean
6
+ mustBeEmpty?: boolean
7
+ silent?: boolean
8
+ }
9
+
10
+ /**
11
+ * Validate a project directory for Polen operations.
12
+ * Used by CLI commands to ensure valid project paths.
13
+ */
14
+ export async function validateProjectDirectory(
15
+ dir: string,
16
+ options: ValidateProjectOptions = {},
17
+ ): Promise<boolean> {
18
+ const { mustExist = true, mustBeEmpty = false, silent = false } = options
19
+
20
+ const stat = await Fs.stat(dir)
21
+
22
+ if (Fs.isNotFoundError(stat)) {
23
+ if (mustExist) {
24
+ if (!silent) consola.error(`Project directory does not exist: ${dir}`)
25
+ return false
26
+ }
27
+ return true
28
+ }
29
+
30
+ if (!stat.isDirectory()) {
31
+ if (!silent) consola.error(`Project path is not a directory: ${dir}`)
32
+ return false
33
+ }
34
+
35
+ if (mustBeEmpty && !(await Fs.isEmptyDir(dir))) {
36
+ if (!silent) consola.error(`Project directory is not empty: ${dir}`)
37
+ return false
38
+ }
39
+
40
+ return true
41
+ }
@@ -162,7 +162,7 @@ const BuildManifest = (config: Config.Config): Vite.Plugin => {
162
162
  const manifest: PolenBuildManifest = {
163
163
  type: config.build.architecture === `ssr` ? `ssr` : `ssg`,
164
164
  version: packageJson.version,
165
- basePath: config.build.base || `/`,
165
+ basePath: config.build.base ?? `/`,
166
166
  }
167
167
 
168
168
  // Emit the manifest as an asset
@@ -209,6 +209,7 @@ export const Core = (config: Config.Config): Vite.PluginOption[] => {
209
209
  paths: config.paths.project,
210
210
  navbar, // Complete navbar with schema and pages
211
211
  server: {
212
+ port: config.server.port,
212
213
  static: {
213
214
  // todo
214
215
  // relative from CWD of process that boots n1ode server
@@ -99,7 +99,7 @@ export const Serve = (
99
99
  config() {
100
100
  return {
101
101
  server: {
102
- port: 3000,
102
+ port: config.server.port,
103
103
  watch: {
104
104
  disableGlobbing: false,
105
105
  },
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Common parameter schemas for CLI commands
5
+ */
6
+
7
+ export const projectParameter = z
8
+ .string()
9
+ .optional()
10
+ .describe(`The path to the project directory. Default is CWD (current working directory).`)
@@ -1,11 +1,16 @@
1
1
  // @ts-nocheck
2
2
  import { Api } from '#api/index'
3
- import { Vite } from '#dep/vite/index'
3
+ import { projectParameter } from '#cli/_/parameters'
4
+ import { ensureOptionalAbsoluteWithCwd } from '#lib/kit-temp'
4
5
  import { Command } from '@molt/command'
5
6
  import { z } from 'zod'
6
7
 
7
8
  const args = Command.create()
8
9
  .parameter(`--debug -d`, z.boolean().default(false))
10
+ .parameter(
11
+ `--project -p`,
12
+ projectParameter,
13
+ )
9
14
  .parameter(
10
15
  `--architecture -a`,
11
16
  Api.Config.BuildArchitecture.default('ssg').describe('Which kind of application architecture to output.'),
@@ -14,6 +19,10 @@ const args = Command.create()
14
19
  `--base -b`,
15
20
  z.string().optional().describe('Base public path for deployment (e.g., /my-project/)'),
16
21
  )
22
+ .parameter(
23
+ `--port`,
24
+ z.number().optional().describe('Default port for the SSR application'),
25
+ )
17
26
  .settings({
18
27
  parameters: {
19
28
  environment: {
@@ -27,14 +36,20 @@ const args = Command.create()
27
36
  })
28
37
  .parse()
29
38
 
30
- // HACK:
31
- // todo
32
- // we don't want to lose pretty preting of defaults in Molt but
33
- // we don't want cli defaults to override explicit inputs in the config file either
34
- // we need something like setset and/or an ability in molt to show a default but then have undefined internally etc.
35
- // and now if user passes --no-debug/ --debug false it has no effect which is wrong since its not via default anymore, ... ugh
39
+ const dir = ensureOptionalAbsoluteWithCwd(args.project)
40
+
41
+ if (!await Api.Project.validateProjectDirectory(dir)) {
42
+ process.exit(1)
43
+ }
44
+
36
45
  await Api.Builder.build({
37
- ...(args.debug === false ? {} : { debug: args.debug }),
38
- ...(args.architecture === 'ssg' ? {} : { architecture: args.architecture }),
39
- ...(args.base ? { base: args.base } : {}),
46
+ dir,
47
+ architecture: args.architecture,
48
+ base: args.base,
49
+ server: {
50
+ port: args.port,
51
+ },
52
+ advanced: {
53
+ debug: args.debug,
54
+ },
40
55
  })
@@ -1,6 +1,23 @@
1
+ import { Api } from '#api/index'
2
+ import { projectParameter } from '#cli/_/parameters'
3
+ import { ensureOptionalAbsoluteWithCwd } from '#lib/kit-temp'
4
+ import { Command } from '@molt/command'
1
5
  import { Fs, Path } from '@wollybeard/kit'
2
6
  import consola from 'consola'
3
7
 
8
+ const args = Command.create()
9
+ .parameter(
10
+ `--project -p`,
11
+ projectParameter,
12
+ )
13
+ .parse()
14
+
15
+ const dir = ensureOptionalAbsoluteWithCwd(args.project)
16
+
17
+ if (!await Api.Project.validateProjectDirectory(dir)) {
18
+ process.exit(1)
19
+ }
20
+
4
21
  const fileName = 'polen.config.ts'
5
22
  const fileContent = `import { Polen } from 'polen'
6
23
 
@@ -9,7 +26,7 @@ export default Polen.defineConfig({
9
26
  })
10
27
  `
11
28
 
12
- const filePath = Path.join(process.cwd(), fileName)
29
+ const filePath = Path.join(dir, fileName)
13
30
 
14
31
  const exists = await Fs.exists(filePath)
15
32
  if (exists) {
@@ -1,4 +1,5 @@
1
1
  // @ts-nocheck
2
+ import { Api } from '#api/index'
2
3
  import { Command } from '@molt/command'
3
4
  import { Err, Fs, Manifest, Name, Path, Str } from '@wollybeard/kit'
4
5
  import * as Ansis from 'ansis'
@@ -48,10 +49,9 @@ const args = Command
48
49
 
49
50
  const getProjectRoot = async (): Promise<string> => {
50
51
  if (args.path) {
51
- const stat = await Fs.stat(args.path)
52
- if (Fs.isNotFoundError(stat)) return args.path
53
- if (stat.isDirectory() && await Fs.isEmptyDir(args.path)) return args.path
54
- consola.error(`The given path ${args.path} already exists and is not an empty directory`)
52
+ if (await Api.Project.validateProjectDirectory(args.path, { mustExist: false, mustBeEmpty: true })) {
53
+ return args.path
54
+ }
55
55
  process.exit(1)
56
56
  }
57
57
 
@@ -1,9 +1,10 @@
1
1
  // @ts-nocheck
2
2
  import { Api } from '#api/index'
3
+ import { projectParameter } from '#cli/_/parameters'
3
4
  import { Vite } from '#dep/vite/index'
4
- import { ensureOptionalAbsolute, ensureOptionalAbsoluteWithCwd } from '#lib/kit-temp'
5
+ import { ensureOptionalAbsoluteWithCwd } from '#lib/kit-temp'
5
6
  import { Command } from '@molt/command'
6
- import { Err, Path } from '@wollybeard/kit'
7
+ import { Err } from '@wollybeard/kit'
7
8
  import { z } from 'zod'
8
9
 
9
10
  const args = Command.create()
@@ -11,12 +12,16 @@ const args = Command.create()
11
12
  .parameter(
12
13
  `--project -p`,
13
14
  // @ts-expect-error
14
- z.string().optional().describe(`The path to the project directory. Default is CWD (current working directory).`),
15
+ projectParameter,
15
16
  )
16
17
  .parameter(
17
18
  `--base -b`,
18
19
  z.string().optional().describe('Base public path for deployment (e.g., /my-project/)'),
19
20
  )
21
+ .parameter(
22
+ `--port`,
23
+ z.number().optional().describe('Port to run the development server on'),
24
+ )
20
25
  .settings({
21
26
  parameters: {
22
27
  environment: {
@@ -30,12 +35,21 @@ const args = Command.create()
30
35
  })
31
36
  .parse()
32
37
 
33
- const dir = ensureOptionalAbsoluteWithCwd(args.project) as string
38
+ const dir = ensureOptionalAbsoluteWithCwd(args.project)
39
+
40
+ if (!await Api.Project.validateProjectDirectory(dir)) {
41
+ process.exit(1)
42
+ }
34
43
 
35
44
  const viteUserConfig = await Api.ConfigResolver.fromFile({
36
45
  dir,
37
46
  overrides: {
38
- ...(args.base ? { build: { base: args.base } } : {}),
47
+ build: {
48
+ base: args.base,
49
+ },
50
+ server: {
51
+ port: args.port,
52
+ },
39
53
  advanced: {
40
54
  debug: args.debug,
41
55
  },
@@ -202,13 +202,15 @@ export const GraphQLDocument: React.FC<GraphQLDocumentProps> = ({
202
202
  data-testid='graphql-document'
203
203
  >
204
204
  {/* Base code block */}
205
- {renderCode ? (
206
- renderCode()
207
- ) : (
208
- <pre className='code-block'>
205
+ {renderCode
206
+ ? (
207
+ renderCode()
208
+ )
209
+ : (
210
+ <pre className='code-block'>
209
211
  <code>{children}</code>
210
- </pre>
211
- )}
212
+ </pre>
213
+ )}
212
214
 
213
215
  {/* Interactive overlay layer */}
214
216
  {!plain && isReady && (
@@ -1,6 +1,6 @@
1
1
  import * as fc from 'fast-check'
2
2
  import { describe, expect, test } from 'vitest'
3
- import { objFilter, ObjOmit, ObjPick, objPolicyFilter } from './kit-temp.js'
3
+ import { objFilter, ObjOmit, ObjPick, objPolicyFilter, spreadShallow } from './kit-temp.js'
4
4
 
5
5
  describe('objPolicyFilter', () => {
6
6
  const testObj = { a: 1, b: 2, c: 3, d: 4 }
@@ -137,3 +137,87 @@ describe('property-based tests', () => {
137
137
  )
138
138
  })
139
139
  })
140
+
141
+ describe('mergeShallow', () => {
142
+ test('merges objects while omitting undefined values', () => {
143
+ const base = { a: 1, b: 2, c: 3 }
144
+ const override = { b: undefined, c: 4, d: 5 }
145
+
146
+ expect(spreadShallow(base, override)).toEqual({ a: 1, b: 2, c: 4, d: 5 })
147
+ })
148
+
149
+ test('handles multiple objects', () => {
150
+ const obj1 = { a: 1, b: 2 }
151
+ const obj2 = { b: undefined, c: 3 }
152
+ const obj3 = { c: undefined, d: 4 }
153
+
154
+ expect(spreadShallow(obj1, obj2, obj3)).toEqual({ a: 1, b: 2, c: 3, d: 4 })
155
+ })
156
+
157
+ test('handles empty objects', () => {
158
+ expect(spreadShallow({}, {})).toEqual({})
159
+ expect(spreadShallow({ a: 1 }, {})).toEqual({ a: 1 })
160
+ expect(spreadShallow({}, { a: 1 })).toEqual({ a: 1 })
161
+ })
162
+
163
+ test('handles single object', () => {
164
+ const obj = { a: 1, b: undefined, c: 3 }
165
+ expect(spreadShallow(obj)).toEqual({ a: 1, c: 3 })
166
+ })
167
+
168
+ test('handles no objects', () => {
169
+ expect(spreadShallow()).toEqual({})
170
+ })
171
+
172
+ test('handles undefined objects', () => {
173
+ const obj = { a: 1, b: 2 }
174
+ expect(spreadShallow(undefined, obj, undefined)).toEqual({ a: 1, b: 2 })
175
+ expect(spreadShallow(obj, undefined)).toEqual({ a: 1, b: 2 })
176
+ expect(spreadShallow(undefined, undefined)).toEqual({})
177
+ })
178
+
179
+ test('preserves null values', () => {
180
+ const obj1 = { a: 1, b: null }
181
+ const obj2 = { b: 2, c: null }
182
+ expect(spreadShallow(obj1, obj2)).toEqual({ a: 1, b: 2, c: null })
183
+ })
184
+
185
+ test('preserves false and 0 values', () => {
186
+ const obj1 = { a: true, b: 1 }
187
+ const obj2 = { a: false, b: 0 }
188
+ expect(spreadShallow(obj1, obj2)).toEqual({ a: false, b: 0 })
189
+ })
190
+
191
+ test('property-based: never includes undefined values', () => {
192
+ fc.assert(
193
+ fc.property(
194
+ fc.array(fc.object({ withUndefined: true })),
195
+ (objects) => {
196
+ const result = spreadShallow(...objects)
197
+ Object.values(result).forEach(value => {
198
+ expect(value).not.toBe(undefined)
199
+ })
200
+ },
201
+ ),
202
+ )
203
+ })
204
+
205
+ test('property-based: later objects override earlier ones', () => {
206
+ fc.assert(
207
+ fc.property(
208
+ fc.object(),
209
+ fc.object(),
210
+ fc.string(),
211
+ fc.anything({ withUndefined: false }),
212
+ (obj1, obj2, key, value) => {
213
+ // Set the same key in both objects
214
+ obj1[key] = 'first'
215
+ obj2[key] = value
216
+
217
+ const result = spreadShallow(obj1, obj2)
218
+ expect(result[key]).toBe(value)
219
+ },
220
+ ),
221
+ )
222
+ })
223
+ })
@@ -592,3 +592,54 @@ export const brand = <$BaseType, $BrandName extends string>(
592
592
  ): Brand<$BaseType, $BrandName> => {
593
593
  return value as Brand<$BaseType, $BrandName>
594
594
  }
595
+
596
+ /**
597
+ * Shallow merge objects while omitting undefined values.
598
+ *
599
+ * This utility simplifies the common pattern of conditionally spreading objects
600
+ * to avoid including undefined values that would override existing values.
601
+ *
602
+ * @param objects - Objects to merge (later objects override earlier ones). Undefined objects are ignored.
603
+ * @returns Merged object with undefined values omitted
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * // Instead of:
608
+ * const overrides = {
609
+ * ...(value1 ? { key1: value1 } : {}),
610
+ * ...(value2 ? { key2: value2 } : {}),
611
+ * }
612
+ *
613
+ * // Use:
614
+ * const overrides = mergeShallow({
615
+ * key1: value1,
616
+ * key2: value2,
617
+ * })
618
+ *
619
+ * // Example with config merging:
620
+ * const config = mergeShallow(
621
+ * defaultConfig,
622
+ * userConfig,
623
+ * { debug: args.debug, base: args.base }
624
+ * )
625
+ * // undefined values in the last object won't override earlier values
626
+ * ```
627
+ */
628
+ export const spreadShallow = <T extends object>(...objects: (T | undefined)[]): T => {
629
+ const result = {} as T
630
+
631
+ for (const obj of objects) {
632
+ if (obj === undefined) continue
633
+
634
+ for (const key in obj) {
635
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
636
+ const value = obj[key]
637
+ if (value !== undefined) {
638
+ result[key] = value
639
+ }
640
+ }
641
+ }
642
+ }
643
+
644
+ return result
645
+ }
@@ -41,7 +41,7 @@ export const _getPathsRecurse = (
41
41
  if (isIndexRoute(route)) {
42
42
  // Index route uses the parent's accumulated path.
43
43
  // If parentPath is empty (implying root), it should be ROOT_PATH
44
- collectedPaths.add(parentPath || ROOT_PATH)
44
+ collectedPaths.add(parentPath ?? ROOT_PATH)
45
45
 
46
46
  continue
47
47
  }
@@ -8,6 +8,7 @@ export interface ProjectData {
8
8
  paths: Config.Config[`paths`][`project`]
9
9
  navbar: NavbarItem[]
10
10
  server: {
11
+ port: number
11
12
  static: {
12
13
  directory: string
13
14
  route: string
@@ -1,5 +1,6 @@
1
1
  import { serve } from '@hono/node-server' // TODO: support non-node platforms.
2
2
  import { neverCase } from '@wollybeard/kit/language'
3
+ import PROJECT_DATA from 'virtual:polen/project/data.jsonsuper'
3
4
  import { createApp } from './app.js'
4
5
  import { generate } from './ssg/generate.js'
5
6
  import { view } from './view.js'
@@ -10,7 +11,7 @@ if (__BUILDING__) {
10
11
  await generate(view)
11
12
  break
12
13
  case `ssr`:
13
- const port = process.env[`PORT`] ? parseInt(process.env[`PORT`]) : 3001 // todo viteConfigResolved.server.port + 1
14
+ const port = process.env[`PORT`] ? parseInt(process.env[`PORT`]) : PROJECT_DATA.server.port
14
15
  const app = createApp()
15
16
  serve({ fetch: app.fetch, port })
16
17
  break