@noego/wood 0.2.1 → 0.3.0

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.
@@ -23,8 +23,8 @@ class Navigation {
23
23
  }
24
24
  return { ...current.params };
25
25
  }
26
- replace(page, params = {}, query = {}) {
27
- const entry = this.createEntry(page, params, query);
26
+ replace(page, params = {}, query = {}, options = {}) {
27
+ const entry = this.createEntry(page, params, query, options);
28
28
  if (this.index < 0) {
29
29
  this.entries.push(entry);
30
30
  this.index = 0;
@@ -39,13 +39,14 @@ class Navigation {
39
39
  payload: {
40
40
  page: entry.page,
41
41
  params: entry.params,
42
- query: entry.query
42
+ query: entry.query,
43
+ options: entry.options
43
44
  }
44
45
  });
45
46
  return this.cloneEntry(entry);
46
47
  }
47
- go(page, params = {}, query = {}) {
48
- const entry = this.createEntry(page, params, query);
48
+ go(page, params = {}, query = {}, options = {}) {
49
+ const entry = this.createEntry(page, params, query, options);
49
50
  if (this.index < this.entries.length - 1) {
50
51
  this.entries = this.entries.slice(0, this.index + 1);
51
52
  }
@@ -59,7 +60,8 @@ class Navigation {
59
60
  payload: {
60
61
  page: entry.page,
61
62
  params: entry.params,
62
- query: entry.query
63
+ query: entry.query,
64
+ options: entry.options
63
65
  }
64
66
  });
65
67
  return this.cloneEntry(entry);
@@ -108,18 +110,20 @@ class Navigation {
108
110
  listener(current);
109
111
  }
110
112
  }
111
- createEntry(page, params, query) {
113
+ createEntry(page, params, query, options) {
112
114
  return {
113
115
  page,
114
116
  params: this.normalizeRecord(params),
115
- query: this.normalizeRecord(query)
117
+ query: this.normalizeRecord(query),
118
+ options: { ...options }
116
119
  };
117
120
  }
118
121
  cloneEntry(entry) {
119
122
  return {
120
123
  page: entry.page,
121
124
  params: { ...entry.params },
122
- query: { ...entry.query }
125
+ query: { ...entry.query },
126
+ options: { ...entry.options }
123
127
  };
124
128
  }
125
129
  normalizeRecord(input) {
@@ -137,7 +141,8 @@ class Navigation {
137
141
  [HISTORY_STATE_MARKER]: true,
138
142
  page: entry.page,
139
143
  params: { ...entry.params },
140
- query: { ...entry.query }
144
+ query: { ...entry.query },
145
+ options: { ...entry.options }
141
146
  };
142
147
  window.history[mode](state, "");
143
148
  }
@@ -155,8 +160,8 @@ function getNavigation() {
155
160
  function goto(delta) {
156
161
  return getNavigation().goto(delta);
157
162
  }
158
- function go(page, params = {}, query = {}) {
159
- return getNavigation().go(page, params, query);
163
+ function go(page, params = {}, query = {}, options = {}) {
164
+ return getNavigation().go(page, params, query, options);
160
165
  }
161
166
  function goback() {
162
167
  return goto(-1);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n}\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nexport interface NavigationOptions {\n preserveLayouts?: boolean;\n}\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n"],"mappings":"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","names":[]}
@@ -41,13 +41,20 @@ function validateControllerValue(controller, contextLabel) {
41
41
  }
42
42
  return controller.trim();
43
43
  }
44
+ function validatePreserveValue(preserve, contextLabel) {
45
+ if (preserve === void 0) return false;
46
+ if (typeof preserve !== "boolean") {
47
+ throw new Error(`${contextLabel} has invalid preserve. Expected a boolean.`);
48
+ }
49
+ return preserve;
50
+ }
44
51
  function parseLayoutEntry(entry, viewKey, index) {
45
52
  if (typeof entry === "string") {
46
53
  const path = entry.trim();
47
54
  if (!path) {
48
55
  throw new Error(`View "${viewKey}" has invalid layout entry at index ${index}. Expected a non-empty path string.`);
49
56
  }
50
- return { path };
57
+ return { path, preserve: false };
51
58
  }
52
59
  const layoutObj = entry;
53
60
  const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;
@@ -58,7 +65,8 @@ function parseLayoutEntry(entry, viewKey, index) {
58
65
  }
59
66
  return {
60
67
  path: layoutPath.trim(),
61
- controller: validateControllerValue(layoutObj.controller, `View "${viewKey}" layout[${index}]`)
68
+ controller: validateControllerValue(layoutObj.controller, `View "${viewKey}" layout[${index}]`),
69
+ preserve: validatePreserveValue(layoutObj.preserve, `View "${viewKey}" layout[${index}]`)
62
70
  };
63
71
  }
64
72
  function isLayoutGroupEntry(entry) {
@@ -85,7 +93,8 @@ function normalizeLayouts(rawLayout, viewKey) {
85
93
  }
86
94
  return {
87
95
  entries: rawLayout.views,
88
- inheritedController: validateControllerValue(rawLayout.controller, `View "${viewKey}" layout`)
96
+ inheritedController: validateControllerValue(rawLayout.controller, `View "${viewKey}" layout`),
97
+ inheritedPreserve: validatePreserveValue(rawLayout.preserve, `View "${viewKey}" layout`)
89
98
  };
90
99
  }
91
100
  if (isLayoutObjectEntry(rawLayout)) {
@@ -122,8 +131,17 @@ function parseViews(yamlContent) {
122
131
  }
123
132
  (_a = parsedLayouts[0]).controller ?? (_a.controller = normalizedLayouts.inheritedController);
124
133
  }
134
+ if (normalizedLayouts.inheritedPreserve) {
135
+ if (parsedLayouts.length === 0) {
136
+ throw new Error(
137
+ `View "${dotPath}" sets layout preserve but has no layout views. Define at least one layout entry.`
138
+ );
139
+ }
140
+ parsedLayouts[0].preserve = true;
141
+ }
125
142
  const layoutPaths = parsedLayouts.map((layout) => layout.path);
126
143
  const layoutControllers = parsedLayouts.map((layout) => layout.controller);
144
+ const layoutPreserve = parsedLayouts.map((layout) => layout.preserve);
127
145
  const viewController = validateControllerValue(entry.controller, `View "${dotPath}"`);
128
146
  const viewDef = {
129
147
  windowName,
@@ -136,6 +154,9 @@ function parseViews(yamlContent) {
136
154
  if (layoutControllers.some((value) => value !== void 0)) {
137
155
  viewDef.layoutControllers = layoutControllers;
138
156
  }
157
+ if (layoutPreserve.some((value) => value)) {
158
+ viewDef.layoutPreserve = layoutPreserve;
159
+ }
139
160
  views.push(viewDef);
140
161
  }
141
162
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/parser/views_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type {\n ViewDef,\n RawViewsDocument,\n RawLayoutEntry,\n RawLayoutObjectEntry,\n RawLayoutConfig,\n RawLayoutGroupEntry,\n} from '../types';\n\nfunction validateControllerValue(controller: unknown, contextLabel: string): string | undefined {\n if (controller === undefined) return undefined;\n if (typeof controller !== 'string' || controller.trim().length === 0) {\n throw new Error(`${contextLabel} has invalid controller. Expected a non-empty string.`);\n }\n return controller.trim();\n}\n\nfunction parseLayoutEntry(entry: RawLayoutEntry, viewKey: string, index: number): { path: string; controller?: string } {\n if (typeof entry === 'string') {\n const path = entry.trim();\n if (!path) {\n throw new Error(`View \"${viewKey}\" has invalid layout entry at index ${index}. Expected a non-empty path string.`);\n }\n return { path };\n }\n\n const layoutObj = entry as RawLayoutObjectEntry;\n const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;\n if (typeof layoutPath !== 'string' || layoutPath.trim().length === 0) {\n throw new Error(\n `View \"${viewKey}\" has invalid layout object at index ${index}. ` +\n 'Expected one of: path, view, or layout as a non-empty string.',\n );\n }\n\n return {\n path: layoutPath.trim(),\n controller: validateControllerValue(layoutObj.controller, `View \"${viewKey}\" layout[${index}]`),\n };\n}\n\nfunction isLayoutGroupEntry(entry: unknown): entry is RawLayoutGroupEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return Object.prototype.hasOwnProperty.call(entry, 'views');\n}\n\nfunction isLayoutObjectEntry(entry: unknown): entry is RawLayoutObjectEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return (\n Object.prototype.hasOwnProperty.call(entry, 'path') ||\n Object.prototype.hasOwnProperty.call(entry, 'view') ||\n Object.prototype.hasOwnProperty.call(entry, 'layout') ||\n Object.prototype.hasOwnProperty.call(entry, 'controller')\n );\n}\n\nfunction normalizeLayouts(rawLayout: RawLayoutConfig | undefined, viewKey: string): {\n entries: RawLayoutEntry[];\n inheritedController?: string;\n} {\n if (rawLayout === undefined) {\n return { entries: [] };\n }\n\n if (Array.isArray(rawLayout)) {\n return { entries: rawLayout };\n }\n\n if (typeof rawLayout === 'string') {\n return { entries: [rawLayout] };\n }\n\n if (isLayoutGroupEntry(rawLayout)) {\n if (!Array.isArray(rawLayout.views)) {\n throw new Error(`View \"${viewKey}\" has invalid layout object. \"views\" must be an array.`);\n }\n\n return {\n entries: rawLayout.views,\n inheritedController: validateControllerValue(rawLayout.controller, `View \"${viewKey}\" layout`),\n };\n }\n\n if (isLayoutObjectEntry(rawLayout)) {\n return { entries: [rawLayout] };\n }\n\n throw new Error(\n `View \"${viewKey}\" has invalid layout definition. Expected an array, a string path, or an object with \"views\".`,\n );\n}\n\n/**\n * Parse views YAML content into a flat array of ViewDef.\n * Supports Wood-native `view:` format (dot-path keys).\n */\nexport function parseViews(yamlContent: string): ViewDef[] {\n const doc = yaml.load(yamlContent) as RawViewsDocument;\n const views: ViewDef[] = [];\n\n // Wood-native `view:` format\n // First segment = window name, rest = page name\n if (doc.view) {\n for (const [dotPath, entry] of Object.entries(doc.view)) {\n const segments = dotPath.split('.');\n const windowName = segments[0];\n const pageName = segments.slice(1).join('.');\n\n const normalizedLayouts = normalizeLayouts(entry.layout, dotPath);\n const parsedLayouts = normalizedLayouts.entries.map((layoutEntry, index) =>\n parseLayoutEntry(layoutEntry, dotPath, index),\n );\n if (normalizedLayouts.inheritedController) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets a layout controller but has no layout views. Define at least one layout entry.`,\n );\n }\n\n const firstLayoutController = parsedLayouts[0].controller;\n if (firstLayoutController && firstLayoutController !== normalizedLayouts.inheritedController) {\n throw new Error(\n `View \"${dotPath}\" has conflicting layout controllers. Use either layout.controller or layout.views[0].controller.`,\n );\n }\n parsedLayouts[0].controller ??= normalizedLayouts.inheritedController;\n }\n\n const layoutPaths = parsedLayouts.map((layout) => layout.path);\n const layoutControllers = parsedLayouts.map((layout) => layout.controller);\n\n const viewController = validateControllerValue(entry.controller, `View \"${dotPath}\"`);\n\n const viewDef: ViewDef = {\n windowName,\n pageName,\n viewPath: entry.view,\n layouts: layoutPaths,\n middleware: entry.middleware,\n controller: viewController,\n };\n\n if (layoutControllers.some((value) => value !== undefined)) {\n viewDef.layoutControllers = layoutControllers;\n }\n\n views.push(viewDef);\n }\n }\n\n return views;\n}\n\n/** Read a YAML file from disk and parse it into ViewDef[]. */\nexport function parseViewsFile(filePath: string): ViewDef[] {\n const content = readFileSync(filePath, 'utf-8');\n return parseViews(content);\n}\n"],"mappings":"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","names":["yaml"]}
1
+ {"version":3,"sources":["../../src/parser/views_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type {\n ViewDef,\n RawViewsDocument,\n RawLayoutEntry,\n RawLayoutObjectEntry,\n RawLayoutConfig,\n RawLayoutGroupEntry,\n} from '../types';\n\nfunction validateControllerValue(controller: unknown, contextLabel: string): string | undefined {\n if (controller === undefined) return undefined;\n if (typeof controller !== 'string' || controller.trim().length === 0) {\n throw new Error(`${contextLabel} has invalid controller. Expected a non-empty string.`);\n }\n return controller.trim();\n}\n\nfunction validatePreserveValue(preserve: unknown, contextLabel: string): boolean {\n if (preserve === undefined) return false;\n if (typeof preserve !== 'boolean') {\n throw new Error(`${contextLabel} has invalid preserve. Expected a boolean.`);\n }\n return preserve;\n}\n\nfunction parseLayoutEntry(entry: RawLayoutEntry, viewKey: string, index: number): { path: string; controller?: string; preserve: boolean } {\n if (typeof entry === 'string') {\n const path = entry.trim();\n if (!path) {\n throw new Error(`View \"${viewKey}\" has invalid layout entry at index ${index}. Expected a non-empty path string.`);\n }\n return { path, preserve: false };\n }\n\n const layoutObj = entry as RawLayoutObjectEntry;\n const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;\n if (typeof layoutPath !== 'string' || layoutPath.trim().length === 0) {\n throw new Error(\n `View \"${viewKey}\" has invalid layout object at index ${index}. ` +\n 'Expected one of: path, view, or layout as a non-empty string.',\n );\n }\n\n return {\n path: layoutPath.trim(),\n controller: validateControllerValue(layoutObj.controller, `View \"${viewKey}\" layout[${index}]`),\n preserve: validatePreserveValue(layoutObj.preserve, `View \"${viewKey}\" layout[${index}]`),\n };\n}\n\nfunction isLayoutGroupEntry(entry: unknown): entry is RawLayoutGroupEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return Object.prototype.hasOwnProperty.call(entry, 'views');\n}\n\nfunction isLayoutObjectEntry(entry: unknown): entry is RawLayoutObjectEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return (\n Object.prototype.hasOwnProperty.call(entry, 'path') ||\n Object.prototype.hasOwnProperty.call(entry, 'view') ||\n Object.prototype.hasOwnProperty.call(entry, 'layout') ||\n Object.prototype.hasOwnProperty.call(entry, 'controller')\n );\n}\n\nfunction normalizeLayouts(rawLayout: RawLayoutConfig | undefined, viewKey: string): {\n entries: RawLayoutEntry[];\n inheritedController?: string;\n inheritedPreserve?: boolean;\n} {\n if (rawLayout === undefined) {\n return { entries: [] };\n }\n\n if (Array.isArray(rawLayout)) {\n return { entries: rawLayout };\n }\n\n if (typeof rawLayout === 'string') {\n return { entries: [rawLayout] };\n }\n\n if (isLayoutGroupEntry(rawLayout)) {\n if (!Array.isArray(rawLayout.views)) {\n throw new Error(`View \"${viewKey}\" has invalid layout object. \"views\" must be an array.`);\n }\n\n return {\n entries: rawLayout.views,\n inheritedController: validateControllerValue(rawLayout.controller, `View \"${viewKey}\" layout`),\n inheritedPreserve: validatePreserveValue(rawLayout.preserve, `View \"${viewKey}\" layout`),\n };\n }\n\n if (isLayoutObjectEntry(rawLayout)) {\n return { entries: [rawLayout] };\n }\n\n throw new Error(\n `View \"${viewKey}\" has invalid layout definition. Expected an array, a string path, or an object with \"views\".`,\n );\n}\n\n/**\n * Parse views YAML content into a flat array of ViewDef.\n * Supports Wood-native `view:` format (dot-path keys).\n */\nexport function parseViews(yamlContent: string): ViewDef[] {\n const doc = yaml.load(yamlContent) as RawViewsDocument;\n const views: ViewDef[] = [];\n\n // Wood-native `view:` format\n // First segment = window name, rest = page name\n if (doc.view) {\n for (const [dotPath, entry] of Object.entries(doc.view)) {\n const segments = dotPath.split('.');\n const windowName = segments[0];\n const pageName = segments.slice(1).join('.');\n\n const normalizedLayouts = normalizeLayouts(entry.layout, dotPath);\n const parsedLayouts = normalizedLayouts.entries.map((layoutEntry, index) =>\n parseLayoutEntry(layoutEntry, dotPath, index),\n );\n if (normalizedLayouts.inheritedController) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets a layout controller but has no layout views. Define at least one layout entry.`,\n );\n }\n\n const firstLayoutController = parsedLayouts[0].controller;\n if (firstLayoutController && firstLayoutController !== normalizedLayouts.inheritedController) {\n throw new Error(\n `View \"${dotPath}\" has conflicting layout controllers. Use either layout.controller or layout.views[0].controller.`,\n );\n }\n parsedLayouts[0].controller ??= normalizedLayouts.inheritedController;\n }\n if (normalizedLayouts.inheritedPreserve) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets layout preserve but has no layout views. Define at least one layout entry.`,\n );\n }\n parsedLayouts[0].preserve = true;\n }\n\n const layoutPaths = parsedLayouts.map((layout) => layout.path);\n const layoutControllers = parsedLayouts.map((layout) => layout.controller);\n const layoutPreserve = parsedLayouts.map((layout) => layout.preserve);\n\n const viewController = validateControllerValue(entry.controller, `View \"${dotPath}\"`);\n\n const viewDef: ViewDef = {\n windowName,\n pageName,\n viewPath: entry.view,\n layouts: layoutPaths,\n middleware: entry.middleware,\n controller: viewController,\n };\n\n if (layoutControllers.some((value) => value !== undefined)) {\n viewDef.layoutControllers = layoutControllers;\n }\n if (layoutPreserve.some((value) => value)) {\n viewDef.layoutPreserve = layoutPreserve;\n }\n\n views.push(viewDef);\n }\n }\n\n return views;\n}\n\n/** Read a YAML file from disk and parse it into ViewDef[]. */\nexport function parseViewsFile(filePath: string): ViewDef[] {\n const content = readFileSync(filePath, 'utf-8');\n return parseViews(content);\n}\n"],"mappings":"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","names":["yaml"]}
@@ -7,13 +7,20 @@ function validateControllerValue(controller, contextLabel) {
7
7
  }
8
8
  return controller.trim();
9
9
  }
10
+ function validatePreserveValue(preserve, contextLabel) {
11
+ if (preserve === void 0) return false;
12
+ if (typeof preserve !== "boolean") {
13
+ throw new Error(`${contextLabel} has invalid preserve. Expected a boolean.`);
14
+ }
15
+ return preserve;
16
+ }
10
17
  function parseLayoutEntry(entry, viewKey, index) {
11
18
  if (typeof entry === "string") {
12
19
  const path = entry.trim();
13
20
  if (!path) {
14
21
  throw new Error(`View "${viewKey}" has invalid layout entry at index ${index}. Expected a non-empty path string.`);
15
22
  }
16
- return { path };
23
+ return { path, preserve: false };
17
24
  }
18
25
  const layoutObj = entry;
19
26
  const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;
@@ -24,7 +31,8 @@ function parseLayoutEntry(entry, viewKey, index) {
24
31
  }
25
32
  return {
26
33
  path: layoutPath.trim(),
27
- controller: validateControllerValue(layoutObj.controller, `View "${viewKey}" layout[${index}]`)
34
+ controller: validateControllerValue(layoutObj.controller, `View "${viewKey}" layout[${index}]`),
35
+ preserve: validatePreserveValue(layoutObj.preserve, `View "${viewKey}" layout[${index}]`)
28
36
  };
29
37
  }
30
38
  function isLayoutGroupEntry(entry) {
@@ -51,7 +59,8 @@ function normalizeLayouts(rawLayout, viewKey) {
51
59
  }
52
60
  return {
53
61
  entries: rawLayout.views,
54
- inheritedController: validateControllerValue(rawLayout.controller, `View "${viewKey}" layout`)
62
+ inheritedController: validateControllerValue(rawLayout.controller, `View "${viewKey}" layout`),
63
+ inheritedPreserve: validatePreserveValue(rawLayout.preserve, `View "${viewKey}" layout`)
55
64
  };
56
65
  }
57
66
  if (isLayoutObjectEntry(rawLayout)) {
@@ -88,8 +97,17 @@ function parseViews(yamlContent) {
88
97
  }
89
98
  (_a = parsedLayouts[0]).controller ?? (_a.controller = normalizedLayouts.inheritedController);
90
99
  }
100
+ if (normalizedLayouts.inheritedPreserve) {
101
+ if (parsedLayouts.length === 0) {
102
+ throw new Error(
103
+ `View "${dotPath}" sets layout preserve but has no layout views. Define at least one layout entry.`
104
+ );
105
+ }
106
+ parsedLayouts[0].preserve = true;
107
+ }
91
108
  const layoutPaths = parsedLayouts.map((layout) => layout.path);
92
109
  const layoutControllers = parsedLayouts.map((layout) => layout.controller);
110
+ const layoutPreserve = parsedLayouts.map((layout) => layout.preserve);
93
111
  const viewController = validateControllerValue(entry.controller, `View "${dotPath}"`);
94
112
  const viewDef = {
95
113
  windowName,
@@ -102,6 +120,9 @@ function parseViews(yamlContent) {
102
120
  if (layoutControllers.some((value) => value !== void 0)) {
103
121
  viewDef.layoutControllers = layoutControllers;
104
122
  }
123
+ if (layoutPreserve.some((value) => value)) {
124
+ viewDef.layoutPreserve = layoutPreserve;
125
+ }
105
126
  views.push(viewDef);
106
127
  }
107
128
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/parser/views_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type {\n ViewDef,\n RawViewsDocument,\n RawLayoutEntry,\n RawLayoutObjectEntry,\n RawLayoutConfig,\n RawLayoutGroupEntry,\n} from '../types';\n\nfunction validateControllerValue(controller: unknown, contextLabel: string): string | undefined {\n if (controller === undefined) return undefined;\n if (typeof controller !== 'string' || controller.trim().length === 0) {\n throw new Error(`${contextLabel} has invalid controller. Expected a non-empty string.`);\n }\n return controller.trim();\n}\n\nfunction parseLayoutEntry(entry: RawLayoutEntry, viewKey: string, index: number): { path: string; controller?: string } {\n if (typeof entry === 'string') {\n const path = entry.trim();\n if (!path) {\n throw new Error(`View \"${viewKey}\" has invalid layout entry at index ${index}. Expected a non-empty path string.`);\n }\n return { path };\n }\n\n const layoutObj = entry as RawLayoutObjectEntry;\n const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;\n if (typeof layoutPath !== 'string' || layoutPath.trim().length === 0) {\n throw new Error(\n `View \"${viewKey}\" has invalid layout object at index ${index}. ` +\n 'Expected one of: path, view, or layout as a non-empty string.',\n );\n }\n\n return {\n path: layoutPath.trim(),\n controller: validateControllerValue(layoutObj.controller, `View \"${viewKey}\" layout[${index}]`),\n };\n}\n\nfunction isLayoutGroupEntry(entry: unknown): entry is RawLayoutGroupEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return Object.prototype.hasOwnProperty.call(entry, 'views');\n}\n\nfunction isLayoutObjectEntry(entry: unknown): entry is RawLayoutObjectEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return (\n Object.prototype.hasOwnProperty.call(entry, 'path') ||\n Object.prototype.hasOwnProperty.call(entry, 'view') ||\n Object.prototype.hasOwnProperty.call(entry, 'layout') ||\n Object.prototype.hasOwnProperty.call(entry, 'controller')\n );\n}\n\nfunction normalizeLayouts(rawLayout: RawLayoutConfig | undefined, viewKey: string): {\n entries: RawLayoutEntry[];\n inheritedController?: string;\n} {\n if (rawLayout === undefined) {\n return { entries: [] };\n }\n\n if (Array.isArray(rawLayout)) {\n return { entries: rawLayout };\n }\n\n if (typeof rawLayout === 'string') {\n return { entries: [rawLayout] };\n }\n\n if (isLayoutGroupEntry(rawLayout)) {\n if (!Array.isArray(rawLayout.views)) {\n throw new Error(`View \"${viewKey}\" has invalid layout object. \"views\" must be an array.`);\n }\n\n return {\n entries: rawLayout.views,\n inheritedController: validateControllerValue(rawLayout.controller, `View \"${viewKey}\" layout`),\n };\n }\n\n if (isLayoutObjectEntry(rawLayout)) {\n return { entries: [rawLayout] };\n }\n\n throw new Error(\n `View \"${viewKey}\" has invalid layout definition. Expected an array, a string path, or an object with \"views\".`,\n );\n}\n\n/**\n * Parse views YAML content into a flat array of ViewDef.\n * Supports Wood-native `view:` format (dot-path keys).\n */\nexport function parseViews(yamlContent: string): ViewDef[] {\n const doc = yaml.load(yamlContent) as RawViewsDocument;\n const views: ViewDef[] = [];\n\n // Wood-native `view:` format\n // First segment = window name, rest = page name\n if (doc.view) {\n for (const [dotPath, entry] of Object.entries(doc.view)) {\n const segments = dotPath.split('.');\n const windowName = segments[0];\n const pageName = segments.slice(1).join('.');\n\n const normalizedLayouts = normalizeLayouts(entry.layout, dotPath);\n const parsedLayouts = normalizedLayouts.entries.map((layoutEntry, index) =>\n parseLayoutEntry(layoutEntry, dotPath, index),\n );\n if (normalizedLayouts.inheritedController) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets a layout controller but has no layout views. Define at least one layout entry.`,\n );\n }\n\n const firstLayoutController = parsedLayouts[0].controller;\n if (firstLayoutController && firstLayoutController !== normalizedLayouts.inheritedController) {\n throw new Error(\n `View \"${dotPath}\" has conflicting layout controllers. Use either layout.controller or layout.views[0].controller.`,\n );\n }\n parsedLayouts[0].controller ??= normalizedLayouts.inheritedController;\n }\n\n const layoutPaths = parsedLayouts.map((layout) => layout.path);\n const layoutControllers = parsedLayouts.map((layout) => layout.controller);\n\n const viewController = validateControllerValue(entry.controller, `View \"${dotPath}\"`);\n\n const viewDef: ViewDef = {\n windowName,\n pageName,\n viewPath: entry.view,\n layouts: layoutPaths,\n middleware: entry.middleware,\n controller: viewController,\n };\n\n if (layoutControllers.some((value) => value !== undefined)) {\n viewDef.layoutControllers = layoutControllers;\n }\n\n views.push(viewDef);\n }\n }\n\n return views;\n}\n\n/** Read a YAML file from disk and parse it into ViewDef[]. */\nexport function parseViewsFile(filePath: string): ViewDef[] {\n const content = readFileSync(filePath, 'utf-8');\n return parseViews(content);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/parser/views_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type {\n ViewDef,\n RawViewsDocument,\n RawLayoutEntry,\n RawLayoutObjectEntry,\n RawLayoutConfig,\n RawLayoutGroupEntry,\n} from '../types';\n\nfunction validateControllerValue(controller: unknown, contextLabel: string): string | undefined {\n if (controller === undefined) return undefined;\n if (typeof controller !== 'string' || controller.trim().length === 0) {\n throw new Error(`${contextLabel} has invalid controller. Expected a non-empty string.`);\n }\n return controller.trim();\n}\n\nfunction validatePreserveValue(preserve: unknown, contextLabel: string): boolean {\n if (preserve === undefined) return false;\n if (typeof preserve !== 'boolean') {\n throw new Error(`${contextLabel} has invalid preserve. Expected a boolean.`);\n }\n return preserve;\n}\n\nfunction parseLayoutEntry(entry: RawLayoutEntry, viewKey: string, index: number): { path: string; controller?: string; preserve: boolean } {\n if (typeof entry === 'string') {\n const path = entry.trim();\n if (!path) {\n throw new Error(`View \"${viewKey}\" has invalid layout entry at index ${index}. Expected a non-empty path string.`);\n }\n return { path, preserve: false };\n }\n\n const layoutObj = entry as RawLayoutObjectEntry;\n const layoutPath = layoutObj.path ?? layoutObj.view ?? layoutObj.layout;\n if (typeof layoutPath !== 'string' || layoutPath.trim().length === 0) {\n throw new Error(\n `View \"${viewKey}\" has invalid layout object at index ${index}. ` +\n 'Expected one of: path, view, or layout as a non-empty string.',\n );\n }\n\n return {\n path: layoutPath.trim(),\n controller: validateControllerValue(layoutObj.controller, `View \"${viewKey}\" layout[${index}]`),\n preserve: validatePreserveValue(layoutObj.preserve, `View \"${viewKey}\" layout[${index}]`),\n };\n}\n\nfunction isLayoutGroupEntry(entry: unknown): entry is RawLayoutGroupEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return Object.prototype.hasOwnProperty.call(entry, 'views');\n}\n\nfunction isLayoutObjectEntry(entry: unknown): entry is RawLayoutObjectEntry {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;\n return (\n Object.prototype.hasOwnProperty.call(entry, 'path') ||\n Object.prototype.hasOwnProperty.call(entry, 'view') ||\n Object.prototype.hasOwnProperty.call(entry, 'layout') ||\n Object.prototype.hasOwnProperty.call(entry, 'controller')\n );\n}\n\nfunction normalizeLayouts(rawLayout: RawLayoutConfig | undefined, viewKey: string): {\n entries: RawLayoutEntry[];\n inheritedController?: string;\n inheritedPreserve?: boolean;\n} {\n if (rawLayout === undefined) {\n return { entries: [] };\n }\n\n if (Array.isArray(rawLayout)) {\n return { entries: rawLayout };\n }\n\n if (typeof rawLayout === 'string') {\n return { entries: [rawLayout] };\n }\n\n if (isLayoutGroupEntry(rawLayout)) {\n if (!Array.isArray(rawLayout.views)) {\n throw new Error(`View \"${viewKey}\" has invalid layout object. \"views\" must be an array.`);\n }\n\n return {\n entries: rawLayout.views,\n inheritedController: validateControllerValue(rawLayout.controller, `View \"${viewKey}\" layout`),\n inheritedPreserve: validatePreserveValue(rawLayout.preserve, `View \"${viewKey}\" layout`),\n };\n }\n\n if (isLayoutObjectEntry(rawLayout)) {\n return { entries: [rawLayout] };\n }\n\n throw new Error(\n `View \"${viewKey}\" has invalid layout definition. Expected an array, a string path, or an object with \"views\".`,\n );\n}\n\n/**\n * Parse views YAML content into a flat array of ViewDef.\n * Supports Wood-native `view:` format (dot-path keys).\n */\nexport function parseViews(yamlContent: string): ViewDef[] {\n const doc = yaml.load(yamlContent) as RawViewsDocument;\n const views: ViewDef[] = [];\n\n // Wood-native `view:` format\n // First segment = window name, rest = page name\n if (doc.view) {\n for (const [dotPath, entry] of Object.entries(doc.view)) {\n const segments = dotPath.split('.');\n const windowName = segments[0];\n const pageName = segments.slice(1).join('.');\n\n const normalizedLayouts = normalizeLayouts(entry.layout, dotPath);\n const parsedLayouts = normalizedLayouts.entries.map((layoutEntry, index) =>\n parseLayoutEntry(layoutEntry, dotPath, index),\n );\n if (normalizedLayouts.inheritedController) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets a layout controller but has no layout views. Define at least one layout entry.`,\n );\n }\n\n const firstLayoutController = parsedLayouts[0].controller;\n if (firstLayoutController && firstLayoutController !== normalizedLayouts.inheritedController) {\n throw new Error(\n `View \"${dotPath}\" has conflicting layout controllers. Use either layout.controller or layout.views[0].controller.`,\n );\n }\n parsedLayouts[0].controller ??= normalizedLayouts.inheritedController;\n }\n if (normalizedLayouts.inheritedPreserve) {\n if (parsedLayouts.length === 0) {\n throw new Error(\n `View \"${dotPath}\" sets layout preserve but has no layout views. Define at least one layout entry.`,\n );\n }\n parsedLayouts[0].preserve = true;\n }\n\n const layoutPaths = parsedLayouts.map((layout) => layout.path);\n const layoutControllers = parsedLayouts.map((layout) => layout.controller);\n const layoutPreserve = parsedLayouts.map((layout) => layout.preserve);\n\n const viewController = validateControllerValue(entry.controller, `View \"${dotPath}\"`);\n\n const viewDef: ViewDef = {\n windowName,\n pageName,\n viewPath: entry.view,\n layouts: layoutPaths,\n middleware: entry.middleware,\n controller: viewController,\n };\n\n if (layoutControllers.some((value) => value !== undefined)) {\n viewDef.layoutControllers = layoutControllers;\n }\n if (layoutPreserve.some((value) => value)) {\n viewDef.layoutPreserve = layoutPreserve;\n }\n\n views.push(viewDef);\n }\n }\n\n return views;\n}\n\n/** Read a YAML file from disk and parse it into ViewDef[]. */\nexport function parseViewsFile(filePath: string): ViewDef[] {\n const content = readFileSync(filePath, 'utf-8');\n return parseViews(content);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/views.ts"],"sourcesContent":["/** Types for views YAML (frontend page/layout definitions) */\n\n/** Fully resolved view definition after YAML parsing */\nexport interface ViewDef {\n windowName: string;\n pageName: string;\n viewPath: string;\n layouts: string[];\n layoutControllers?: Array<string | undefined>;\n middleware?: string[];\n parameters?: ParameterDef[];\n controller?: string;\n}\n\n/** OpenAPI-style parameter definition for view routes */\nexport interface ParameterDef {\n name: string;\n in: string;\n required?: boolean;\n schema?: Record<string, unknown>;\n}\n\n/** Raw views YAML document before resolution */\nexport interface RawViewsDocument {\n view?: Record<string, RawViewEntry>;\n}\n\n/** A simple view entry using dot-path keys */\nexport interface RawViewEntry {\n view: string;\n layout?: RawLayoutConfig;\n middleware?: string[];\n controller?: string;\n}\n\nexport type RawLayoutConfig = RawLayoutEntry[] | RawLayoutEntry | RawLayoutGroupEntry;\n\nexport type RawLayoutEntry = string | RawLayoutObjectEntry;\n\nexport interface RawLayoutObjectEntry {\n path?: string;\n view?: string;\n layout?: string;\n controller?: string;\n}\n\nexport interface RawLayoutGroupEntry {\n controller?: string;\n views: RawLayoutEntry[];\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../../src/types/views.ts"],"sourcesContent":["/** Types for views YAML (frontend page/layout definitions) */\n\n/** Fully resolved view definition after YAML parsing */\nexport interface ViewDef {\n windowName: string;\n pageName: string;\n viewPath: string;\n layouts: string[];\n layoutControllers?: Array<string | undefined>;\n layoutPreserve?: boolean[];\n middleware?: string[];\n parameters?: ParameterDef[];\n controller?: string;\n}\n\n/** OpenAPI-style parameter definition for view routes */\nexport interface ParameterDef {\n name: string;\n in: string;\n required?: boolean;\n schema?: Record<string, unknown>;\n}\n\n/** Raw views YAML document before resolution */\nexport interface RawViewsDocument {\n view?: Record<string, RawViewEntry>;\n}\n\n/** A simple view entry using dot-path keys */\nexport interface RawViewEntry {\n view: string;\n layout?: RawLayoutConfig;\n middleware?: string[];\n controller?: string;\n}\n\nexport type RawLayoutConfig = RawLayoutEntry[] | RawLayoutEntry | RawLayoutGroupEntry;\n\nexport type RawLayoutEntry = string | RawLayoutObjectEntry;\n\nexport interface RawLayoutObjectEntry {\n path?: string;\n view?: string;\n layout?: string;\n controller?: string;\n preserve?: boolean;\n}\n\nexport interface RawLayoutGroupEntry {\n controller?: string;\n preserve?: boolean;\n views: RawLayoutEntry[];\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -6,6 +6,7 @@ interface ViewDef {
6
6
  viewPath: string;
7
7
  layouts: string[];
8
8
  layoutControllers?: Array<string | undefined>;
9
+ layoutPreserve?: boolean[];
9
10
  middleware?: string[];
10
11
  parameters?: ParameterDef[];
11
12
  controller?: string;
@@ -35,9 +36,11 @@ interface RawLayoutObjectEntry {
35
36
  view?: string;
36
37
  layout?: string;
37
38
  controller?: string;
39
+ preserve?: boolean;
38
40
  }
39
41
  interface RawLayoutGroupEntry {
40
42
  controller?: string;
43
+ preserve?: boolean;
41
44
  views: RawLayoutEntry[];
42
45
  }
43
46
 
@@ -6,6 +6,7 @@ interface ViewDef {
6
6
  viewPath: string;
7
7
  layouts: string[];
8
8
  layoutControllers?: Array<string | undefined>;
9
+ layoutPreserve?: boolean[];
9
10
  middleware?: string[];
10
11
  parameters?: ParameterDef[];
11
12
  controller?: string;
@@ -35,9 +36,11 @@ interface RawLayoutObjectEntry {
35
36
  view?: string;
36
37
  layout?: string;
37
38
  controller?: string;
39
+ preserve?: boolean;
38
40
  }
39
41
  interface RawLayoutGroupEntry {
40
42
  controller?: string;
43
+ preserve?: boolean;
41
44
  views: RawLayoutEntry[];
42
45
  }
43
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/wood",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",