poveste 0.16.1 → 0.16.2

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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Resolves `specifier` as Node would from `from`, to a real path, or `undefined`
3
+ * when nothing answers.
4
+ */
5
+ export type Resolver = (specifier: string, from: string) => string | undefined;
6
+ export declare const nodeResolver: Resolver;
7
+ export interface VendoredAlias {
8
+ /** The bare package the alias points at, `vue`. */
9
+ name: string;
10
+ /** The range it is pinned to, `^3.5.26`, or `''` when the spec carries none. */
11
+ range: string;
12
+ }
13
+ /**
14
+ * What each vendored dependency aliases, by the name it is installed under:
15
+ * `{ 'poveste-vue': { name: 'vue', range: '^3.5.26' } }` from
16
+ * `"poveste-vue": "npm:vue@^3.5.26"`.
17
+ *
18
+ * Derived from the manifest rather than listed, because the set is whatever
19
+ * `@poveste/vendors` declares and a list here would keep working while silently
20
+ * covering one name fewer. The range comes back with the name because the caller
21
+ * needs both and this is the only place the spec is parsed.
22
+ */
23
+ export declare function vendoredAliases(dependencies: Record<string, string> | undefined): Record<string, VendoredAlias>;
24
+ /**
25
+ * Whether `version` satisfies a caret range, for the caret ranges `@poveste/vendors`
26
+ * actually declares and **nothing else**.
27
+ *
28
+ * A range this cannot read with certainty is refused rather than approximated. That
29
+ * is the point: `poveste` has no `semver` dependency, a patch release is the wrong
30
+ * place to add one, and a hand-rolled `satisfies` that silently mis-handles a form it
31
+ * was never given is the failure this repo keeps producing. So `^X.Y.Z` with a major
32
+ * of at least 1 is answered, and `^0.x`, a prerelease, a comparator set or anything
33
+ * else returns false and leaves the chrome on the copy it can certainly run.
34
+ */
35
+ export declare function satisfiesCaret(version: string | undefined, range: string | undefined): boolean;
36
+ /**
37
+ * Points the chrome's Vue at the project's, when they are two copies.
38
+ *
39
+ * `@poveste/vendors` depends on `poveste-vue`, an npm alias of `vue`. An alias is
40
+ * a **distinct package name**, so a package manager installs a second physical copy
41
+ * whatever the project already has — the project's Vue satisfying the aliased range
42
+ * does not collapse them, and `resolve.dedupe` cannot, because it matches on name.
43
+ *
44
+ * Two copies is two reactivity systems. The chrome's components render with the
45
+ * vendored one while `RouterView` — reached through `poveste-vue-router`, whose own
46
+ * `vue` import is bare — reacts with the project's, so `RouterView`'s first render
47
+ * is never invalidated and a Vue consumer's `poveste dev` showed a sidebar, a
48
+ * breadcrumb and no story at all. Nothing warns: neither copy is wrong, they simply
49
+ * cannot see each other (#1060).
50
+ *
51
+ * Only Vue. The reactivity identity is what breaks, and `@poveste/plugin-vue`
52
+ * declaring `vue` as a peer is what licenses pointing the chrome at the project's
53
+ * copy — no peer range licenses the same for pinia, vue-router, VueUse or Iconify,
54
+ * and making the chrome's pinned copies authoritative for a project's own story code
55
+ * is a larger change than this defect asks for.
56
+ *
57
+ * Empty when the project has no Vue of its own, when the two already resolve to one
58
+ * file, or when the project's Vue does not satisfy the range vendors pins — in each
59
+ * case the vendored copy is what the chrome should keep. Refusing on anything it
60
+ * cannot read with certainty is deliberate: the failure of this function is a chrome
61
+ * that keeps a Vue it can certainly run, and the failure of guessing is a chrome
62
+ * handed one it cannot.
63
+ *
64
+ * The range is checked rather than assumed from the peer floor. A project only holds
65
+ * a Vue of its own because a framework plugin's peer range put it there — except that
66
+ * `--legacy-peer-deps` is exactly how a project ends up holding one the peer range
67
+ * would have refused (#1062), which leaves a reader on, say, Vue 3.2 with a chrome
68
+ * collapsed onto a Vue that cannot run it. Such a project is already broken without
69
+ * this fix, but "already broken" is not a reason to hand it a different break.
70
+ */
71
+ export declare function collapseVendoredVue(options: {
72
+ root: string;
73
+ resolve?: Resolver;
74
+ }): Record<string, string>;
@@ -0,0 +1,157 @@
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'pathe';
4
+ export const nodeResolver = (specifier, from) => {
5
+ try {
6
+ // `createRequire` resolves relative to the *dirname* of what it is given, so a
7
+ // directory has to be named as a file inside it or every lookup starts one
8
+ // level too high.
9
+ return realpathSync(createRequire(join(from, 'noop.js')).resolve(specifier));
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ };
15
+ /**
16
+ * What each vendored dependency aliases, by the name it is installed under:
17
+ * `{ 'poveste-vue': { name: 'vue', range: '^3.5.26' } }` from
18
+ * `"poveste-vue": "npm:vue@^3.5.26"`.
19
+ *
20
+ * Derived from the manifest rather than listed, because the set is whatever
21
+ * `@poveste/vendors` declares and a list here would keep working while silently
22
+ * covering one name fewer. The range comes back with the name because the caller
23
+ * needs both and this is the only place the spec is parsed.
24
+ */
25
+ export function vendoredAliases(dependencies) {
26
+ const aliases = {};
27
+ for (const [installedAs, spec] of Object.entries(dependencies ?? {})) {
28
+ if (!spec.startsWith('npm:')) {
29
+ continue;
30
+ }
31
+ // Split on the last `@` rather than matching a name pattern: a scoped package
32
+ // opens with one, so the separator is positional and a regex that allows both
33
+ // is the kind the linter rejects for backtracking.
34
+ const target = spec.slice('npm:'.length);
35
+ const at = target.lastIndexOf('@');
36
+ aliases[installedAs] = at > 0
37
+ ? { name: target.slice(0, at), range: target.slice(at + 1) }
38
+ : { name: target, range: '' };
39
+ }
40
+ return aliases;
41
+ }
42
+ /**
43
+ * Whether `version` satisfies a caret range, for the caret ranges `@poveste/vendors`
44
+ * actually declares and **nothing else**.
45
+ *
46
+ * A range this cannot read with certainty is refused rather than approximated. That
47
+ * is the point: `poveste` has no `semver` dependency, a patch release is the wrong
48
+ * place to add one, and a hand-rolled `satisfies` that silently mis-handles a form it
49
+ * was never given is the failure this repo keeps producing. So `^X.Y.Z` with a major
50
+ * of at least 1 is answered, and `^0.x`, a prerelease, a comparator set or anything
51
+ * else returns false and leaves the chrome on the copy it can certainly run.
52
+ */
53
+ export function satisfiesCaret(version, range) {
54
+ const wanted = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range ?? '');
55
+ const have = /^(\d+)\.(\d+)\.(\d+)$/.exec(version ?? '');
56
+ if (!wanted || !have) {
57
+ return false;
58
+ }
59
+ const [wantedMajor, wantedMinor, wantedPatch] = wanted.slice(1).map(Number);
60
+ const [major, minor, patch] = have.slice(1).map(Number);
61
+ // `^0.x` narrows to the minor, and no vendored range uses it. Refuse rather than
62
+ // implement a rule nothing here exercises.
63
+ if (wantedMajor === 0 || wantedMajor === undefined || major === undefined) {
64
+ return false;
65
+ }
66
+ if (major !== wantedMajor) {
67
+ return false;
68
+ }
69
+ if (minor !== wantedMinor) {
70
+ return minor > wantedMinor;
71
+ }
72
+ return patch >= wantedPatch;
73
+ }
74
+ /**
75
+ * Points the chrome's Vue at the project's, when they are two copies.
76
+ *
77
+ * `@poveste/vendors` depends on `poveste-vue`, an npm alias of `vue`. An alias is
78
+ * a **distinct package name**, so a package manager installs a second physical copy
79
+ * whatever the project already has — the project's Vue satisfying the aliased range
80
+ * does not collapse them, and `resolve.dedupe` cannot, because it matches on name.
81
+ *
82
+ * Two copies is two reactivity systems. The chrome's components render with the
83
+ * vendored one while `RouterView` — reached through `poveste-vue-router`, whose own
84
+ * `vue` import is bare — reacts with the project's, so `RouterView`'s first render
85
+ * is never invalidated and a Vue consumer's `poveste dev` showed a sidebar, a
86
+ * breadcrumb and no story at all. Nothing warns: neither copy is wrong, they simply
87
+ * cannot see each other (#1060).
88
+ *
89
+ * Only Vue. The reactivity identity is what breaks, and `@poveste/plugin-vue`
90
+ * declaring `vue` as a peer is what licenses pointing the chrome at the project's
91
+ * copy — no peer range licenses the same for pinia, vue-router, VueUse or Iconify,
92
+ * and making the chrome's pinned copies authoritative for a project's own story code
93
+ * is a larger change than this defect asks for.
94
+ *
95
+ * Empty when the project has no Vue of its own, when the two already resolve to one
96
+ * file, or when the project's Vue does not satisfy the range vendors pins — in each
97
+ * case the vendored copy is what the chrome should keep. Refusing on anything it
98
+ * cannot read with certainty is deliberate: the failure of this function is a chrome
99
+ * that keeps a Vue it can certainly run, and the failure of guessing is a chrome
100
+ * handed one it cannot.
101
+ *
102
+ * The range is checked rather than assumed from the peer floor. A project only holds
103
+ * a Vue of its own because a framework plugin's peer range put it there — except that
104
+ * `--legacy-peer-deps` is exactly how a project ends up holding one the peer range
105
+ * would have refused (#1062), which leaves a reader on, say, Vue 3.2 with a chrome
106
+ * collapsed onto a Vue that cannot run it. Such a project is already broken without
107
+ * this fix, but "already broken" is not a reason to hand it a different break.
108
+ */
109
+ export function collapseVendoredVue(options) {
110
+ const { root } = options;
111
+ const resolve = options.resolve ?? nodeResolver;
112
+ // From the vendors directory, not from the project: that is where the import
113
+ // happens, and an isolated `node_modules` layout makes `poveste-vue` invisible
114
+ // from the project root while the chrome resolves it perfectly well.
115
+ // Through an exported subpath: vendors' `exports` map does not list
116
+ // `./package.json`, so resolving that name directly fails.
117
+ const vendorsEntry = resolve('@poveste/vendors/vue', root);
118
+ const vendorsDir = vendorsEntry ? join(vendorsEntry, '..') : undefined;
119
+ if (!vendorsDir) {
120
+ return {};
121
+ }
122
+ const vueAlias = Object.entries(vendoredAliases(readJson(join(vendorsDir, 'package.json'))?.dependencies))
123
+ .find(([, alias]) => alias.name === 'vue');
124
+ if (!vueAlias) {
125
+ return {};
126
+ }
127
+ const [installedAs, { range }] = vueAlias;
128
+ const vendored = resolve(installedAs, vendorsDir);
129
+ const project = resolve('vue', root);
130
+ if (!vendored || !project || vendored === project) {
131
+ return {};
132
+ }
133
+ if (!satisfiesCaret(versionOf('vue/package.json', resolve, root), range)) {
134
+ return {};
135
+ }
136
+ // To the bare name, not to `project`. A resolved path is whatever the `require`
137
+ // condition answers — for Vue that is a CJS shim, and handing the chrome that
138
+ // replaced a split reactivity system with a browser app that does not boot at all.
139
+ // The name lets Vite resolve its own entry and condition, and `resolve.dedupe`
140
+ // above then genuinely covers the result.
141
+ return { [installedAs]: 'vue' };
142
+ }
143
+ function versionOf(specifier, resolve, root) {
144
+ const manifest = resolve(specifier, root);
145
+ if (!manifest) {
146
+ return undefined;
147
+ }
148
+ return readJson(manifest)?.version;
149
+ }
150
+ function readJson(file) {
151
+ try {
152
+ return JSON.parse(readFileSync(file, 'utf8'));
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
package/dist/node/vite.js CHANGED
@@ -11,6 +11,7 @@ import { notifyStoryChange } from './stories.js';
11
11
  import { chromeCssScopePlugin, globalStylesPlugin, userCssScopePlugin, } from './style-isolation/index.js';
12
12
  import { applyHeadTransform } from './util/head.js';
13
13
  import { viteCommand, viteMode } from './util/vite-mode.js';
14
+ import { collapseVendoredVue } from './vendors.js';
14
15
  import { createVirtualFilesPlugin } from './virtual/vite-plugin.js';
15
16
  const require = createRequire(import.meta.url);
16
17
  export async function mergePovesteViteConfig(viteConfig, ctx) {
@@ -153,6 +154,14 @@ export async function getViteConfigWithPlugins(isServer, ctx) {
153
154
  ],
154
155
  alias: {
155
156
  'poveste-style': join(APP_PATH, process.env['POVESTE_DEV'] ? 'app/style/main.pcss' : 'style.css'),
157
+ // `dedupe` above cannot reach this one: the chrome's Vue is installed
158
+ // under a different name, and dedupe matches on the name (#1060).
159
+ //
160
+ // Not under `POVESTE_DEV`. That path already aliases bare `vue` to
161
+ // `@poveste/vendors/vue`, so sending `poveste-vue` back to `vue` would
162
+ // point the vendored entry at itself — the empty re-export its own
163
+ // comment warns about.
164
+ ...(isServer || process.env['POVESTE_DEV'] ? {} : collapseVendoredVue({ root: ctx.root })),
156
165
  },
157
166
  ...(isServer
158
167
  ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poveste",
3
3
  "type": "module",
4
- "version": "0.16.1",
4
+ "version": "0.16.2",
5
5
  "description": "Interactive component playgrounds for Vue, Nuxt, Svelte, SvelteKit and Quasar — a drop-in fork of histoire, with Quasar support histoire does not ship.",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan",
@@ -74,10 +74,10 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@11ty/gray-matter": "^3.0.0",
77
- "@poveste/app": "^0.16.1",
78
- "@poveste/controls": "^0.16.1",
79
- "@poveste/shared": "^0.16.1",
80
- "@poveste/vendors": "^0.16.1",
77
+ "@poveste/app": "^0.16.2",
78
+ "@poveste/controls": "^0.16.2",
79
+ "@poveste/shared": "^0.16.2",
80
+ "@poveste/vendors": "^0.16.2",
81
81
  "change-case": "^5.4.4",
82
82
  "chokidar": "^5.0.0",
83
83
  "defu": "^6.1.4",