@poveste/shared 0.14.0 → 0.16.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.
@@ -42,7 +42,7 @@ function objectToSourceLines(object, seen, indentCount = 0) {
42
42
  lines.push('{');
43
43
  lines.push(...createLines(1, (lines) => {
44
44
  for (const key in object) {
45
- const value = object[key];
45
+ const value = Reflect.get(object, key);
46
46
  let printedKey = key;
47
47
  if (KEY_ESCAPE_REG.test(key)) {
48
48
  printedKey = `'${printedKey}'`;
@@ -66,11 +66,11 @@ function arrayToSourceLines(array, seen, indentCount = 0) {
66
66
  addLinesFromValue(lines, value, '', ',', seen);
67
67
  }
68
68
  });
69
- if (contentLines.length === 0) {
69
+ const [first] = contentLines;
70
+ if (!first) {
70
71
  lines.push('[]');
71
72
  }
72
73
  else if (contentLines.length <= MAX_SINGLE_LINE_ARRAY_LENGTH && !contentLines.some(line => line.spaces > 1)) {
73
- const [first] = contentLines;
74
74
  first.line = contentLines.map(({ line }) => line.substring(0, line.length - 1)).join(', ');
75
75
  first.line = `[${first.line}]`;
76
76
  first.spaces--;
@@ -120,7 +120,11 @@ function addLinesFromValue(lines, value, before, after, seen) {
120
120
  lines.push(before + result + after);
121
121
  }
122
122
  function wrap(lines, before, after) {
123
- lines[0].line = before + lines[0].line;
124
- lines[lines.length - 1].line += after;
123
+ const first = lines[0];
124
+ const last = lines.at(-1);
125
+ if (first)
126
+ first.line = before + first.line;
127
+ if (last)
128
+ last.line += after;
125
129
  return lines;
126
130
  }
@@ -4,7 +4,9 @@ export function indent(lines, count = 1) {
4
4
  export function unindent(code) {
5
5
  const lines = code.split('\n');
6
6
  let indentLevel = -1;
7
- let indentText;
7
+ // Empty until a non-blank line is seen: `replace(undefined, '')` would look for
8
+ // the literal text "undefined" (TS6 reports it as used before assignment).
9
+ let indentText = '';
8
10
  const linesToAnalyze = lines.filter(line => line.trim().length > 0);
9
11
  for (const line of linesToAnalyze) {
10
12
  const match = /^\s*/.exec(line);
@@ -51,7 +53,7 @@ export function createAutoBuildingObject(format, specialKeysHandler, key = '', d
51
53
  if (!cache[p]) {
52
54
  const childKey = key ? `${key}.${p.toString()}` : p.toString();
53
55
  const child = createAutoBuildingObject(format, specialKeysHandler, childKey, depth + 1);
54
- cache[p] = { key: childKey, ...child };
56
+ cache[p] = child;
55
57
  }
56
58
  return cache[p].proxy;
57
59
  },
package/dist/setup.d.ts CHANGED
@@ -2,6 +2,11 @@ export type SetupModule = Record<string, unknown> | undefined;
2
2
  /**
3
3
  * Reads a setup hook (`setupVanilla`, `setupVue3`, ...) out of a setup module.
4
4
  *
5
+ * `retired` names hooks a plugin has stopped accepting. Nothing passes a
6
+ * non-empty list today; it exists so that removing a name at 1.0 is a string
7
+ * moving between two arrays rather than a failure mode designed under release
8
+ * pressure.
9
+ *
5
10
  * Takes the module as an argument instead of letting callers access it inline:
6
11
  * the setup modules are namespace imports of virtual or user-provided files that
7
12
  * aren't guaranteed to declare every hook, and a static `namespace.setupVue3`
@@ -9,4 +14,4 @@ export type SetupModule = Record<string, unknown> | undefined;
9
14
  * consumer build. Passing the namespace through a call forces it to be
10
15
  * materialized, so the lookup happens at runtime where it belongs.
11
16
  */
12
- export declare function getSetupHook<T>(mod: SetupModule, name: string | string[]): T | undefined;
17
+ export declare function getSetupHook<T>(mod: SetupModule, name: string | string[], retired?: string[]): T | undefined;
package/dist/setup.js CHANGED
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Reads a setup hook (`setupVanilla`, `setupVue3`, ...) out of a setup module.
3
3
  *
4
+ * `retired` names hooks a plugin has stopped accepting. Nothing passes a
5
+ * non-empty list today; it exists so that removing a name at 1.0 is a string
6
+ * moving between two arrays rather than a failure mode designed under release
7
+ * pressure.
8
+ *
4
9
  * Takes the module as an argument instead of letting callers access it inline:
5
10
  * the setup modules are namespace imports of virtual or user-provided files that
6
11
  * aren't guaranteed to declare every hook, and a static `namespace.setupVue3`
@@ -8,9 +13,28 @@
8
13
  * consumer build. Passing the namespace through a call forces it to be
9
14
  * materialized, so the lookup happens at runtime where it belongs.
10
15
  */
11
- export function getSetupHook(mod, name) {
16
+ export function getSetupHook(mod, name, retired = []) {
12
17
  const names = typeof name === 'string' ? [name] : name;
13
18
  const present = names.filter(candidate => typeof mod?.[candidate] === 'function');
19
+ // Dropping a name is otherwise silent: the export stops being looked up, and a
20
+ // file that has it does nothing at all — no error, no warning, no hook. The
21
+ // people most likely to hold one are working from an older recipe, which is
22
+ // the worst audience for a silent failure.
23
+ //
24
+ // `retired` is a parameter rather than a constant so the mechanism can be
25
+ // exercised with a fixture list while every production list is still empty.
26
+ // An empty detector that has only ever been seen to pass cannot be told apart
27
+ // from one that does not work (#157).
28
+ const dropped = retired.filter(candidate => typeof mod?.[candidate] === 'function');
29
+ if (dropped.length > 0) {
30
+ // Two different situations, and the message has to say which: with no live
31
+ // name exported the setup does not run at all, and with one it does — so
32
+ // "nothing runs" would send someone to debug a setup that is working.
33
+ const consequence = present.length > 0
34
+ ? `${present[0]} runs; the old export is ignored.`
35
+ : `Rename to ${names[names.length - 1]} — nothing runs until you do.`;
36
+ console.warn(`[poveste] Setup file exports ${dropped.join(', ')}, which ${dropped.length === 1 ? 'is' : 'are'} no longer read. ${consequence}`);
37
+ }
14
38
  // Earlier names win, so a plugin lists its established hook first and a newer
15
39
  // alias after it: an existing setup file keeps the exact behaviour it had.
16
40
  // Running both instead would apply the same setup twice, which for Vue means
@@ -19,5 +43,6 @@ export function getSetupHook(mod, name) {
19
43
  console.warn(`[poveste] Setup file exports ${present.length} interchangeable setup hooks (${present.join(', ')}). `
20
44
  + `Only ${present[0]} runs. Keep one — they are aliases, not separate hooks.`);
21
45
  }
22
- return present.length > 0 ? mod[present[0]] : undefined;
46
+ const [first] = present;
47
+ return first ? mod[first] : undefined;
23
48
  }
package/dist/state.d.ts CHANGED
@@ -1,5 +1,32 @@
1
1
  export declare function clone(data: any): any;
2
- export declare function omit(data: any, keys: string[]): {};
2
+ export declare function omit(data: any, keys: string[]): Record<string, any>;
3
+ /**
4
+ * Whether Vue has marked this value raw, and the walkers must stop here.
5
+ *
6
+ * Read as a property rather than through Vue's `isReactive`, because this
7
+ * package does not depend on Vue — and because the flag is what has to be
8
+ * asked anyway: `markRaw` defines `__v_skip` **non-enumerable**, so it crosses
9
+ * neither `structuredClone` nor JSON, and by the time a value has been through
10
+ * the sandbox bridge the mark is gone (#957).
11
+ *
12
+ * A marked value looks plain to every predicate here: a component's exposed
13
+ * object and `markRaw({ … })` both have `Object.prototype` and own enumerable
14
+ * keys, so without this the compare and the copy descend into whatever is
15
+ * behind them.
16
+ *
17
+ * The read is guarded because state is user data and may be a proxy with a
18
+ * throwing trap; an unanswerable value is treated as unmarked, which is the
19
+ * behaviour this had before.
20
+ */
21
+ export declare function isMarkedRaw(value: any): boolean;
22
+ /**
23
+ * Exported because `poveste-app`'s walker has to draw the same line. The two
24
+ * disagreeing about what a non-plain value is was #977: the walker flattened a
25
+ * `Date` to `{}` before `isEquivalent` ever saw it, so the defence below —
26
+ * written for exactly that case — was handed two empty plain objects and
27
+ * reported them equivalent.
28
+ */
29
+ export declare function isPlainObject(value: any): boolean;
3
30
  /**
4
31
  * Structural comparison, deliberately narrow: it recurses into plain objects and
5
32
  * arrays and compares everything else by `Object.is`.
package/dist/state.js CHANGED
@@ -1,3 +1,5 @@
1
+ // `any` in and out, as it has always been published: #709 decides whether this
2
+ // helper stays public, and typing it here would change it for callers first.
1
3
  export function clone(data) {
2
4
  try {
3
5
  return structuredClone(data);
@@ -22,7 +24,43 @@ export function omit(data, keys) {
22
24
  }
23
25
  return copy;
24
26
  }
25
- function isPlainObject(value) {
27
+ /**
28
+ * Whether Vue has marked this value raw, and the walkers must stop here.
29
+ *
30
+ * Read as a property rather than through Vue's `isReactive`, because this
31
+ * package does not depend on Vue — and because the flag is what has to be
32
+ * asked anyway: `markRaw` defines `__v_skip` **non-enumerable**, so it crosses
33
+ * neither `structuredClone` nor JSON, and by the time a value has been through
34
+ * the sandbox bridge the mark is gone (#957).
35
+ *
36
+ * A marked value looks plain to every predicate here: a component's exposed
37
+ * object and `markRaw({ … })` both have `Object.prototype` and own enumerable
38
+ * keys, so without this the compare and the copy descend into whatever is
39
+ * behind them.
40
+ *
41
+ * The read is guarded because state is user data and may be a proxy with a
42
+ * throwing trap; an unanswerable value is treated as unmarked, which is the
43
+ * behaviour this had before.
44
+ */
45
+ export function isMarkedRaw(value) {
46
+ if (value === null || typeof value !== 'object') {
47
+ return false;
48
+ }
49
+ try {
50
+ return value.__v_skip === true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ /**
57
+ * Exported because `poveste-app`'s walker has to draw the same line. The two
58
+ * disagreeing about what a non-plain value is was #977: the walker flattened a
59
+ * `Date` to `{}` before `isEquivalent` ever saw it, so the defence below —
60
+ * written for exactly that case — was handed two empty plain objects and
61
+ * reported them equivalent.
62
+ */
63
+ export function isPlainObject(value) {
26
64
  if (value === null || typeof value !== 'object') {
27
65
  return false;
28
66
  }
@@ -47,6 +85,12 @@ export function isEquivalent(a, b, seen) {
47
85
  if (Object.is(a, b)) {
48
86
  return true;
49
87
  }
88
+ // Same reason as the prototype test below, reached differently: a marked
89
+ // value is one Vue itself refuses to look inside, and two distinct ones are
90
+ // not the same value whatever their keys say.
91
+ if (isMarkedRaw(a) || isMarkedRaw(b)) {
92
+ return false;
93
+ }
50
94
  const bothArrays = Array.isArray(a) && Array.isArray(b);
51
95
  if (!bothArrays && !(isPlainObject(a) && isPlainObject(b))) {
52
96
  return false;
@@ -234,7 +278,7 @@ export function diffState(baseline, next) {
234
278
  * `Object.is`, so a reference is the identity the comparison is about.
235
279
  */
236
280
  function copyState(value, seen = new WeakMap()) {
237
- if (!Array.isArray(value) && !isPlainObject(value)) {
281
+ if (isMarkedRaw(value) || (!Array.isArray(value) && !isPlainObject(value))) {
238
282
  return value;
239
283
  }
240
284
  if (seen.has(value)) {
@@ -1,13 +1,13 @@
1
1
  export declare const SANDBOX_ERROR = "__poveste:sandbox-error";
2
2
  export interface StoryError {
3
3
  message: string;
4
- stack?: string;
4
+ stack?: string | undefined;
5
5
  /**
6
6
  * Which occupant threw. A warm realm is retargeted rather than reloaded
7
7
  * (#240), so the host has to reject a report from the previous one.
8
8
  */
9
- storyId?: string;
10
- variantId?: string;
9
+ storyId?: string | undefined;
10
+ variantId?: string | undefined;
11
11
  }
12
12
  export declare function toStoryError(error: unknown): Pick<StoryError, 'message' | 'stack'>;
13
13
  export declare function reportStoryError(error: unknown, occupant?: Pick<StoryError, 'storyId' | 'variantId'>): void;
@@ -1,4 +1,4 @@
1
- import type MarkdownIt from 'markdown-it';
1
+ import type { MarkdownIt } from 'markdown-it';
2
2
  import type { UseHeadInput } from 'unhead/types';
3
3
  import type { UserConfig as ViteConfig, ConfigEnv as ViteConfigEnv } from 'vite';
4
4
  import type { Plugin } from './plugin.js';
@@ -40,32 +40,69 @@ export interface ModuleLoader {
40
40
  loadModule: (file: string) => Promise<any>;
41
41
  destroy: () => void;
42
42
  }
43
+ /** What changed under a path passed to `api.watch`. */
44
+ export type WatchEvent = 'add' | 'change' | 'unlink';
45
+ /** Called by `api.watch` with what happened and the absolute path it happened to. */
46
+ export type WatchCallback = (event: WatchEvent, path: string) => Awaitable<unknown>;
43
47
  /**
44
- * What every plugin hook receives. `colors`, `path` and `fs` are Poveste's own
45
- * picocolors, pathe and fs-extra, handed over so a plugin can use them without
46
- * adding a second copy to the tree.
48
+ * What every plugin hook receives.
47
49
  *
48
50
  * Prefer `log`/`warn`/`error` over `console`: they prefix the plugin's name, so
49
51
  * a line in a busy build says what printed it.
50
52
  */
51
53
  export interface PluginApiBase {
54
+ /**
55
+ * Poveste's own picocolors.
56
+ *
57
+ * @deprecated Import `picocolors` directly. Works for the rest of 0.x and is removable at 1.0 (#356).
58
+ */
52
59
  colors: typeof pc;
60
+ /**
61
+ * Poveste's own pathe.
62
+ *
63
+ * @deprecated Import `node:path`, or `pathe` for forward slashes on Windows. Works for the rest of
64
+ * 0.x and is removable at 1.0 (#356).
65
+ */
53
66
  path: typeof path;
67
+ /**
68
+ * Poveste's own fs-extra.
69
+ *
70
+ * @deprecated Import `node:fs` or `node:fs/promises` directly. Works for the rest of 0.x and is
71
+ * removable at 1.0 (#356).
72
+ */
54
73
  fs: typeof fs;
55
74
  moduleLoader: ModuleLoader;
56
75
  readonly pluginTempDir: string;
57
- log: (...msg: any[]) => void;
58
- warn: (...msg: any[]) => void;
59
- error: (...msg: any[]) => void;
76
+ log: (...msg: unknown[]) => void;
77
+ warn: (...msg: unknown[]) => void;
78
+ error: (...msg: unknown[]) => void;
60
79
  getStories: () => ServerStory[];
61
80
  addStoryFile: (file: string) => void;
62
81
  getConfig: () => PovesteConfig;
82
+ /**
83
+ * Calls `callback` when a file under `paths` is added, changed or removed after
84
+ * the call. What already exists raises nothing.
85
+ *
86
+ * Poveste owns the watcher and closes it with the dev server, however the server
87
+ * ends, so a plugin has nothing to release. The returned function stops watching
88
+ * sooner. A callback that throws or rejects is logged with the plugin's name.
89
+ *
90
+ * Only `poveste dev` watches. In a build this does nothing, because a watcher
91
+ * opened while a failed build closes its server is what kept that build's
92
+ * process alive (#434).
93
+ */
94
+ watch: (paths: string | string[], callback: WatchCallback) => () => Promise<void>;
63
95
  }
64
96
  /**
65
- * {@link PluginApiBase} plus the file watcher, passed to `onDev`. Only the dev
66
- * server has one — a build never watches.
97
+ * {@link PluginApiBase} as passed to `onDev`.
67
98
  */
68
99
  export interface PluginApiDev extends PluginApiBase {
100
+ /**
101
+ * Poveste's own chokidar module. A watcher opened from it is the plugin's to close.
102
+ *
103
+ * @deprecated Use {@link PluginApiBase.watch}, which Poveste closes with the dev server. Works for
104
+ * the rest of 0.x and is removable at 1.0 (#356).
105
+ */
69
106
  watcher: typeof chokidar;
70
107
  }
71
108
  /**
@@ -75,26 +75,26 @@ export interface VariantMeta extends CommonMeta {
75
75
  export interface Variant {
76
76
  id: string;
77
77
  title: string;
78
- icon?: string;
79
- iconColor?: string;
80
- setupApp?: (payload: any) => unknown;
81
- slots?: () => {
78
+ icon?: string | undefined;
79
+ iconColor?: string | undefined;
80
+ setupApp?: ((payload: any) => unknown) | undefined;
81
+ slots?: (() => {
82
82
  default: any;
83
83
  controls: any;
84
84
  source: any;
85
- };
85
+ }) | undefined;
86
86
  state: any;
87
- source?: string;
88
- responsiveDisabled?: boolean;
89
- autoPropsDisabled?: boolean;
90
- configReady?: boolean;
91
- previewReady?: boolean;
92
- meta?: VariantMeta;
87
+ source?: string | undefined;
88
+ responsiveDisabled?: boolean | undefined;
89
+ autoPropsDisabled?: boolean | undefined;
90
+ configReady?: boolean | undefined;
91
+ previewReady?: boolean | undefined;
92
+ meta?: VariantMeta | undefined;
93
93
  }
94
94
  export interface PropDefinition {
95
95
  name: string;
96
- types?: string[];
97
- required?: boolean;
96
+ types?: string[] | undefined;
97
+ required?: boolean | undefined;
98
98
  default?: any;
99
99
  }
100
100
  export interface AutoPropComponentDefinition {
@@ -123,7 +123,7 @@ export interface ServerStoryFile {
123
123
  /**
124
124
  * Generated path for tree UI
125
125
  */
126
- treePath?: string[];
126
+ treePath?: string[] | undefined;
127
127
  /**
128
128
  * Use the module id in imports to allow HMR
129
129
  */
@@ -131,23 +131,23 @@ export interface ServerStoryFile {
131
131
  /**
132
132
  * Resolved story data from story file execution
133
133
  */
134
- story?: ServerStory;
134
+ story?: ServerStory | undefined;
135
135
  /**
136
136
  * Data sent to user tree config functions
137
137
  */
138
- treeFile?: ServerTreeFile;
138
+ treeFile?: ServerTreeFile | undefined;
139
139
  /**
140
140
  * Is virtual module
141
141
  */
142
- virtual?: boolean;
142
+ virtual?: boolean | undefined;
143
143
  /**
144
144
  * Virtual module code
145
145
  */
146
- moduleCode?: string;
146
+ moduleCode?: string | undefined;
147
147
  /**
148
148
  * Related markdown docs
149
149
  */
150
- markdownFile?: ServerMarkdownFile;
150
+ markdownFile?: ServerMarkdownFile | undefined;
151
151
  }
152
152
  export interface ServerMarkdownFile {
153
153
  id: string;
@@ -155,28 +155,28 @@ export interface ServerMarkdownFile {
155
155
  absolutePath: string;
156
156
  isRelatedToStory: boolean;
157
157
  frontmatter?: any;
158
- html?: string;
159
- content?: string;
160
- storyFile?: ServerStoryFile;
158
+ html?: string | undefined;
159
+ content?: string | undefined;
160
+ storyFile?: ServerStoryFile | undefined;
161
161
  }
162
162
  export interface ServerStory {
163
163
  id: string;
164
164
  title: string;
165
- group?: string;
165
+ group?: string | undefined;
166
166
  variants: ServerVariant[];
167
- layout?: StoryLayout;
168
- icon?: string;
169
- iconColor?: string;
170
- docsOnly?: boolean;
171
- docsText?: string;
172
- meta?: StoryMeta;
167
+ layout?: StoryLayout | undefined;
168
+ icon?: string | undefined;
169
+ iconColor?: string | undefined;
170
+ docsOnly?: boolean | undefined;
171
+ docsText?: string | undefined;
172
+ meta?: StoryMeta | undefined;
173
173
  }
174
174
  export interface ServerVariant {
175
175
  id: string;
176
176
  title: string;
177
- icon?: string;
178
- iconColor?: string;
179
- meta?: VariantMeta;
177
+ icon?: string | undefined;
178
+ iconColor?: string | undefined;
179
+ meta?: VariantMeta | undefined;
180
180
  }
181
181
  export interface ServerTreeFile {
182
182
  title: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/shared",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "description": "Shared utilities for Poveste",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -33,24 +33,26 @@
33
33
  "dist"
34
34
  ],
35
35
  "engines": {
36
- "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
36
+ "node": ">=22.22.2"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "vite": "^8.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@poveste/vendors": "^0.14.0",
42
+ "@poveste/vendors": "^0.16.0",
43
43
  "@types/fs-extra": "^11.0.4",
44
- "@types/markdown-it": "^14.1.2",
45
- "chokidar": "^4.0.1",
46
- "pathe": "^1.1.2",
44
+ "chokidar": "^5.0.0",
45
+ "markdown-it": "^15.0.2",
46
+ "pathe": "^2.0.3",
47
47
  "picocolors": "^1.1.1",
48
- "unhead": "^3.1.0"
48
+ "unhead": "^3.4.1"
49
49
  },
50
50
  "devDependencies": {
51
- "typescript": "5.6.3",
52
- "vite": "^8.2.0",
53
- "vitest": "^5.0.0"
51
+ "@types/node": "^26.6.3",
52
+ "typescript": "6.0.3",
53
+ "vite": "^8.3.1",
54
+ "vitest": "^5.0.2",
55
+ "vue-router": "^5.3.1"
54
56
  },
55
57
  "scripts": {
56
58
  "build": "rimraf dist && tsc",