@stealthscale/vite-plugin-product 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stealth Scale B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @stealthscale/vite-plugin-product
2
+
3
+ `@stealthscale/vite-plugin-product` composes a product from its plugins while the product builds. It
4
+ checks every installed plugin against the others, serves the resolved product as `virtual:product`,
5
+ builds each plugin's lazy modules into a chunk of its own, and writes the catalogues the product's
6
+ services read.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add -D @stealthscale/vite-plugin-product
12
+ ```
13
+
14
+ The package peers on `vite`, `@stealthscale/sdk-core`, `@stealthscale/vite-plugin-base` and
15
+ `@stealthscale/vite-plugin-i18n`. A product adds it through `@stealthscale/vite-config-product`,
16
+ beside the layers of `@stealthscale/vite-config-i18n`.
17
+
18
+ ## Usage
19
+
20
+ The product states its plugins in `src/product.ts`, whose default export is the definition:
21
+
22
+ ```ts
23
+ import { defineProduct, installed } from "@stealthscale/sdk-core";
24
+ import { manifest as timeOff } from "@acme/plugin-time-off";
25
+
26
+ export default defineProduct({
27
+ name: "product.name",
28
+ plugins: [installed(timeOff)],
29
+ productId: "people",
30
+ version: "1.4.0",
31
+ });
32
+ ```
33
+
34
+ The application reads the resolved product from `virtual:product`:
35
+
36
+ ```ts
37
+ import { product } from "virtual:product";
38
+ ```
39
+
40
+ Add the module's type with a triple-slash directive from a file the project compiles:
41
+
42
+ ```ts
43
+ /// <reference types="@stealthscale/vite-plugin-product/client" />
44
+ ```
45
+
46
+ `product({ definition })` names another module. `src/product.ts` is the default.
47
+ `product({ standalone })` serves the standalone page of a plugin, described in
48
+ [The standalone page](#the-standalone-page).
49
+
50
+ ## The composition
51
+
52
+ When a build or a dev server starts, the plugin composes the product:
53
+
54
+ 1. It imports the definition through a Vite environment of its own, with `noExternal: true` and the
55
+ application's server conditions. Every module the import reads is evaluated there, so a contract
56
+ and a manifest entry load in Node without React.
57
+ 2. It finds each installed plugin's web package and contract package among the modules the import
58
+ evaluated. The package that contains the first module to export the manifest, in evaluation
59
+ order, is the web package. The same rule finds the contract package. A manifest the definition
60
+ builds itself, which no module exports, belongs to the definition module's package. No package is
61
+ imported for the search alone.
62
+ 3. It leaves out a web package outside the product's own package and its dependencies. The resolver
63
+ then reports the plugin.
64
+ 4. It reads the words from the `stealth:i18n` plugin: the fallback language's catalogue of each
65
+ namespace, and the packages that publish each one. The plugin's contract package and the
66
+ application's own catalogues are left out of the publishers.
67
+ 5. It calls `resolveProduct` with the definition, the web packages, the words and the
68
+ `validateHotkey` of `@tanstack/hotkeys` 0.10.0, the version `@tanstack/react-hotkeys` binds keys
69
+ with.
70
+
71
+ A configuration without the `stealth:i18n` plugin fails the build and names
72
+ `@stealthscale/vite-config-i18n`.
73
+
74
+ ## Faults
75
+
76
+ A build that finds a problem fails with every problem as `<path>: <reason>`, followed by a hint for
77
+ each contract package whose catalogues the i18n layer does not follow. The hint states the scope to
78
+ add. The bundler prints each warning, and the build continues.
79
+
80
+ Under a dev server, `virtual:product` serves the last product that resolved. It throws the problems
81
+ while no product has resolved yet.
82
+
83
+ A definition that fails to load fails with its error and the rule a web package's main entry
84
+ follows: it imports its components and React through lazy importers alone. A definition without an
85
+ object as its default export fails and names the file.
86
+
87
+ ## Chunks
88
+
89
+ The plugin adds one code-splitting group, at priority 4: above the shared group and below the vendor
90
+ group of `@stealthscale/vite-config`. The group assigns every module of an installed plugin's web
91
+ package to the chunk `plugin-<id>`, so each plugin's pages, extensions, commands and settings
92
+ sections build into one chunk. A module that an entry imports statically is left out of the group.
93
+ The web package's main entry and its manifest load with the entry chunk.
94
+
95
+ ## Catalogues
96
+
97
+ The client environment of a build writes three files under `.product` in its output:
98
+
99
+ | File | Contains |
100
+ | -------------------------- | --------------------------------------------------------- |
101
+ | `.product/access.json` | Every permission, resource kind, role and entitlement |
102
+ | `.product/flags.json` | Every flag, one kill switch per installed plugin included |
103
+ | `.product/operations.json` | Every query and mutation, in the order of the plugins |
104
+
105
+ Each description is translated into every language the declaring plugin's catalogues contain. The
106
+ access and flag lists are sorted by qualified id. A dev server and a server environment write none.
107
+
108
+ ## Changes on a dev server
109
+
110
+ The plugin watches every file the composition read. A change to one of them composes the product
111
+ again:
112
+
113
+ - Under a dev server that serves one module per file, `hotUpdate` invalidates `virtual:product` and
114
+ reloads the page.
115
+ - Under a dev server that bundles, `watchChange` reloads the page. `virtual:product` lists every
116
+ composition file as a watched file, so the rebuild loads it again.
117
+ - A composition with problems shows them in the error overlay and keeps the last product that
118
+ resolved.
119
+ - One composition runs at a time, in the order of the changes. The changes reported during a
120
+ composition compose the product once more after it ends.
121
+ - Every composition imports the definition through one environment, which the plugin closes when the
122
+ bundle closes. A composition after a change transforms only the changed files again: 18 ms at 30
123
+ plugins and 45 ms at 100, measured on generated products.
124
+
125
+ A watching build composes the product again at its next build start.
126
+
127
+ ## The standalone page
128
+
129
+ `product({ standalone })` serves a plugin's standalone page from the plugin's own web package. The
130
+ package adds it through `product.standalone()` of `@stealthscale/vite-config-product`, which applies
131
+ it under a dev server alone.
132
+
133
+ - Once the configuration resolves, the plugin writes the definition of the product that runs the
134
+ plugin to `node_modules/.stealth/standalone/product.ts`. The definition passes the module that
135
+ `manifest` names, `src/manifest.ts` by default, and each contract package in `beside` to
136
+ `standaloneFrom` of `@stealthscale/sdk-host/standalone`. The plugin leaves a definition with the
137
+ same source untouched, so the watcher does not report a change. Where `definition` names a module,
138
+ the plugin writes none and composes that module.
139
+ - The definition module's package is the web package of the plugin, and of each plugin installed
140
+ from its contract alone.
141
+ - The plugin serves the page's document as `index.html` at the project root, and its entry as
142
+ `virtual:standalone`. The entry imports `@stealthscale/theme/styles.css`. It loads `virtual:i18n`,
143
+ `virtual:product`, `@stealthscale/sdk-host/standalone/app` and the module `glyphs` names through
144
+ `import()`, then calls `renderStandalone` with the page's `locales` and `themes`.
145
+ - The page loads the definition as `virtual:standalone-product`, the generated source with the
146
+ manifest module imported from the project root. An application's chunks place every module under
147
+ `node_modules` in the vendor chunk, and the generated file there would import the entry chunk that
148
+ imports it.
149
+ - The entry runs every component after the React refresh runtime installs. The bundling dev server
150
+ installs the runtime in the entry's own body, after the chunks the entry imports statically, and
151
+ the bundler may move a component the entry shares with a lazy module into such a chunk.
152
+ - A server that bundles serves the document at every address without a file on disk. A server that
153
+ serves one module per file reads `index.html` from disk, so it does not serve the page.
154
+
155
+ ## Licence
156
+
157
+ MIT. See [LICENSE](LICENSE).
package/client.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Declares the module the product plugin serves, so an import of it type-checks.
3
+ *
4
+ * @remarks
5
+ * An application loads this declaration through a triple-slash directive that names
6
+ * `@stealthscale/vite-plugin-product/client`, in a file it compiles. The module exists only in a
7
+ * build that includes the plugin.
8
+ */
9
+
10
+ declare module "virtual:product" {
11
+ import { type Product } from "@stealthscale/sdk-core";
12
+
13
+ /**
14
+ * The product as the build resolved it, with each installed plugin's manifest.
15
+ */
16
+ export const product: Product;
17
+ }
@@ -0,0 +1 @@
1
+ {"$schema":"http://cyclonedx.org/schema/bom-1.7.schema.json","bomFormat":"CycloneDX","specVersion":"1.7","version":1,"metadata":{"lifecycles":[{"phase":"build"}],"tools":{"components":[{"type":"application","name":"rolldown","version":"1.2.11"},{"type":"application","name":"vite","version":"8.3.1"},{"type":"library","name":"core","group":"@arethetypeswrong","version":"0.18.5","bom-ref":"pkg:npm/%40arethetypeswrong/core@0.18.5","author":"Andrew Branch","description":"The API behind arethetypeswrong.github.io","hashes":[{"alg":"SHA-512","content":"f72b63cc6c318e6f54cfb23d6af7dbb79be542deae93db914444b3263aabce797a58abc8e9dc184e8faf277534d96aeafe64778aa97f3f386f1e129d2480230d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40arethetypeswrong/core@0.18.5","externalReferences":[{"url":"https://github.com/arethetypeswrong/arethetypeswrong.github.io.git#packages/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20"}]},{"type":"library","name":"sdk-core","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/sdk-core@0.1.0","description":"Declares the contracts and manifests of plugins, evaluates their conditions, and resolves a product from them, without loading another package at run time.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/sdk-core@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#sdk/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/sdk/core#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"testing-config","group":"@stealthscale","version":"0.5.0","bom-ref":"pkg:npm/%40stealthscale/testing-config@0.5.0","description":"Lists every part of the house contract a config, plugin or library package breaks.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/testing-config@0.5.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/testing-config","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/testing-config#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"testing","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/testing@0.3.0","description":"Supplies the scratch workspaces, manifests, element measurements and plugin drivers a specification needs.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/testing@0.3.0","externalReferences":[{"url":"git+https://github.com/stealth-scale/scale.git#packages/testing","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-typescript","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","description":"Sets the TypeScript options every stealthscale package compiles with.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-typescript","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-typescript#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config","group":"@stealthscale","version":"0.6.0","bom-ref":"pkg:npm/%40stealthscale/vite-config@0.6.0","description":"Configures a package from five Vite+ tiers and a named layer for every block.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config@0.6.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-plugin-base","group":"@stealthscale","version":"0.2.0","bom-ref":"pkg:npm/%40stealthscale/vite-plugin-base@0.2.0","description":"Turns a name and one write step into a bundler plugin that reads the module graph.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-plugin-base@0.2.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-plugin-base","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-plugin-base#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-plugin-i18n","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-plugin-i18n@0.1.0","description":"Finds every catalogue an application can reach, types their keys, and answers the module it loads them from.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-plugin-i18n@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-plugin-i18n","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-plugin-i18n#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"node","group":"@types","version":"26.6.3","bom-ref":"pkg:npm/%40types/node@26.6.3","description":"TypeScript definitions for node","hashes":[{"alg":"SHA-512","content":"76ca8c410a1e4cbaaef70ca70c3d34ab9ef798dceca3721d40e01f1d126a2c27260853e81a8f40d5b0e951cbfff6d9caa44ad05aff6e29e19f977ec4212d3662"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/node@26.6.3","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/node","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"coverage-v8","group":"@vitest","version":"5.0.1","bom-ref":"pkg:npm/%40vitest/coverage-v8@5.0.1","author":"Anthony Fu <anthonyfu117@hotmail.com>","description":"V8 coverage provider for Vitest","hashes":[{"alg":"SHA-512","content":"1510bc0028ae742ddd23a313a65cd14985870d19c8bf620f7dbcece0576824db0c6a5ff7e6c595f21c087d5f1972acd23f2fae5c778c54e36df4212ab4e535ef"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40vitest/coverage-v8@5.0.1","externalReferences":[{"url":"https://github.com/vitest-dev/vitest/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/vitest-dev/vitest.git#packages/coverage-v8","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://vitest.dev/guide/coverage","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"vite-plus-core","group":"@voidzero-dev","version":"1.0.0","bom-ref":"pkg:npm/%40voidzero-dev/vite-plus-core@1.0.0","author":"VoidZero Inc.","description":"The Unified Toolchain for the Web","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40voidzero-dev/vite-plus-core@1.0.0","externalReferences":[{"url":"https://github.com/voidzero-dev/vite-plus/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/voidzero-dev/vite-plus.git#packages/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://viteplus.dev/guide","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^20.19.0 || ^22.18.0 || >=24.11.0"}]},{"type":"library","name":"typescript","version":"7.0.2","bom-ref":"pkg:npm/typescript@7.0.2","author":"Microsoft Corp.","description":"TypeScript is a language for application scale JavaScript development","hashes":[{"alg":"SHA-512","content":"f0561abbdea8dcd28e85b8ca8bfa8dbc6fd6e63873c646dd9b9b23f406d9ff94f9b16aa7de12602df1b1dbbb11299593bf2cc23fc74b4414dfe6d05349155434"}],"licenses":[{"license":{"id":"Apache-2.0"}}],"purl":"pkg:npm/typescript@7.0.2","externalReferences":[{"url":"https://github.com/microsoft/TypeScript/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/microsoft/TypeScript.git","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://www.typescriptlang.org/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=16.20.0"}]},{"type":"library","name":"vitest","version":"5.0.1","bom-ref":"pkg:npm/vitest@5.0.1","author":"Anthony Fu <anthonyfu117@hotmail.com>","description":"Next generation testing framework powered by Vite","hashes":[{"alg":"SHA-512","content":"880f799506ca124bebb5391d0209d66d77db8a95a28967bf8439763f9b4c8ba585c03efa1b43715c01a9fccf4439c62ea5e18946beb0a6931cec2a00e354d08e"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/vitest@5.0.1","externalReferences":[{"url":"https://github.com/vitest-dev/vitest/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/vitest-dev/vitest.git#packages/vitest","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://vitest.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^22.12.0 || ^24.0.0 || >=26.0.0"}]}]},"component":{"type":"library","name":"vite-plugin-product","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-plugin-product@0.1.0","description":"Composes a product from its plugins while it builds: checks every plugin against the others, resolves the product, and serves it as virtual:product.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-plugin-product@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-plugin-product","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-plugin-product#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}],"evidence":{"licenses":[{"license":{"name":"file: LICENSE","text":{"content":"TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyNiBTdGVhbHRoIFNjYWxlIEIuVi4KClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==","contentType":"text/plain","encoding":"base64"}}}]}},"supplier":{"name":"Stealth Scale B.V.","url":["https://stealthscale.io"]}},"components":[],"dependencies":[{"ref":"pkg:npm/%40stealthscale/vite-plugin-product@0.1.0"}]}
@@ -0,0 +1,78 @@
1
+ import { Plugin } from "vite";
2
+ import "@stealthscale/sdk-core";
3
+ //#region src/module.d.ts
4
+ /**
5
+ * The specifier an application imports the product from.
6
+ */
7
+ export declare const ID = "virtual:product";
8
+ //#endregion
9
+ //#region src/standalone.d.ts
10
+ /**
11
+ * Writes the standalone page of a plugin: the definition of the product that runs the plugin, the
12
+ * page's document and the page's entry.
13
+ */
14
+ /**
15
+ * Lists what the standalone page of a plugin runs and offers.
16
+ */
17
+ interface StandalonePage {
18
+ /**
19
+ * Package names of the contracts installed beside the plugin, each from its contract alone. None
20
+ * where left out.
21
+ */
22
+ readonly beside?: readonly string[] | undefined;
23
+ /**
24
+ * Path of a module, from the project root, whose `glyphs` export the page renders with. None
25
+ * where left out.
26
+ */
27
+ readonly glyphs?: string | undefined;
28
+ /**
29
+ * The locales the page offers, the first its fallback. American English alone where left out.
30
+ */
31
+ readonly locales?: readonly [string, ...string[]] | undefined;
32
+ /**
33
+ * Path of the module that exports the plugin's manifest, from the project root.
34
+ * `src/manifest.ts` where left out.
35
+ */
36
+ readonly manifest?: string | undefined;
37
+ /**
38
+ * The themes the page offers, by name. The application's one theme where left out.
39
+ */
40
+ readonly themes?: readonly string[] | undefined;
41
+ }
42
+ //#endregion
43
+ //#region src/plugin.d.ts
44
+ /**
45
+ * Lists the options of the product plugin.
46
+ */
47
+ export interface ProductOptions {
48
+ /**
49
+ * Path of the module whose default export is the product's definition, from the project root.
50
+ * `src/product.ts` by default, and the generated definition under `standalone`.
51
+ */
52
+ readonly definition?: string | undefined;
53
+ /**
54
+ * The standalone page of a plugin, which the plugin serves at every address without a file. No
55
+ * page where left out.
56
+ */
57
+ readonly standalone?: StandalonePage | undefined;
58
+ }
59
+ /**
60
+ * Creates the plugin that composes a product from its plugins, serves `virtual:product`, builds
61
+ * each plugin's lazy modules into one chunk, and writes the access, flag and operation catalogues
62
+ * of a build.
63
+ *
64
+ * @remarks
65
+ * The product is composed when the build starts, and when a dev server starts. A build fails on
66
+ * a problem and prints each warning. A dev server serves no product until the definition
67
+ * resolves, and `virtual:product` then fails with the problems. A change to a file the
68
+ * composition read composes the product again: the page reloads, or shows the problems in the
69
+ * error overlay and keeps the last product. The client build writes the catalogues under
70
+ * `.product` in its output. Under `standalone`, the plugin writes the definition of the product
71
+ * that runs the plugin at `node_modules/.stealth/standalone/product.ts` once the configuration
72
+ * resolves, unless `definition` names one. It serves the page's document as `index.html`, its
73
+ * entry as `virtual:standalone`, and the generated definition as `virtual:standalone-product`.
74
+ * @param options - The definition's path, and the standalone page.
75
+ */
76
+ export declare function product(options?: ProductOptions): Plugin;
77
+ //#endregion
78
+ export type { StandalonePage };
package/dist/index.mjs ADDED
@@ -0,0 +1,947 @@
1
+ import { dirname, join, posix, relative, resolve, sep } from "node:path";
2
+ import { normalizePath } from "vite";
3
+ import { lineOf, resolveProduct } from "@stealthscale/sdk-core";
4
+ import { dependencies, importer, literal, manifestAt, owning, quoted, text } from "@stealthscale/vite-plugin-base";
5
+ import { LOCALES, cataloguesOf } from "@stealthscale/vite-plugin-i18n";
6
+ import { validateHotkey } from "@tanstack/hotkeys";
7
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ //#region src/catalogues.ts
9
+ /**
10
+ * Returns true where a value is an object whose members can be read by name.
11
+ */
12
+ function isRecord$1(value) {
13
+ return typeof value === "object" && value !== null;
14
+ }
15
+ /**
16
+ * Returns the text at a dotted key of a catalogue, or undefined where the catalogue has none.
17
+ */
18
+ function textAt(words, key) {
19
+ let node = words;
20
+ for (const part of key.split(".")) node = isRecord$1(node) ? node[part] : void 0;
21
+ return typeof node === "string" ? node : void 0;
22
+ }
23
+ /**
24
+ * Returns a function that translates a plugin's key into each language of its catalogues, reading
25
+ * each pair of language and namespace from the catalogue plugin once.
26
+ *
27
+ * @throws {@link Error} From the returned function, when a catalogue cannot be read, naming the
28
+ * plugin and the language.
29
+ */
30
+ function describerOf(api) {
31
+ const languages = /* @__PURE__ */ new Map();
32
+ const read = /* @__PURE__ */ new Map();
33
+ for (const { language, namespace } of api.catalogues()) languages.set(namespace, /* @__PURE__ */ new Set([...languages.get(namespace) ?? [], language]));
34
+ /**
35
+ * Returns the words of one pair, read once.
36
+ *
37
+ * @throws {@link Error} When the catalogue plugin cannot read the pair.
38
+ */
39
+ const wordsOf = (language, plugin) => {
40
+ const pair = `${language}/${plugin}`;
41
+ try {
42
+ if (!read.has(pair)) read.set(pair, api.words(language, plugin));
43
+ } catch (error) {
44
+ throw new Error(`The catalogue of ${plugin} in ${language} cannot be read: ${String(error)}`, { cause: error });
45
+ }
46
+ return read.get(pair);
47
+ };
48
+ return (plugin, key) => Object.fromEntries([...languages.get(plugin) ?? []].toSorted().flatMap((language) => {
49
+ const text = textAt(wordsOf(language, plugin), key);
50
+ return text === void 0 ? [] : [[language, text]];
51
+ }));
52
+ }
53
+ /**
54
+ * Returns the catalogue entry of a resolved name.
55
+ */
56
+ function entryOf$1(name, describe) {
57
+ return {
58
+ deprecated: name.deprecated,
59
+ description: describe(name.plugin, name.description),
60
+ id: name.id,
61
+ plugin: name.plugin
62
+ };
63
+ }
64
+ /**
65
+ * Orders two entries by their qualified ids.
66
+ */
67
+ function byId(first, second) {
68
+ return first.id < second.id ? -1 : 1;
69
+ }
70
+ /**
71
+ * Returns the catalogue entry of a query or a mutation.
72
+ *
73
+ * @param declaration - The resolved query or mutation.
74
+ * @param kind - Whether the declaration is a query or a mutation.
75
+ */
76
+ function operationOf(declaration, kind) {
77
+ return {
78
+ id: declaration.operation.id,
79
+ kind,
80
+ name: declaration.id.slice(declaration.plugin.length + 1),
81
+ plugin: declaration.plugin
82
+ };
83
+ }
84
+ /**
85
+ * Builds the access, flag and operation catalogues of a resolved product.
86
+ *
87
+ * @remarks
88
+ * The access and flag lists are sorted by qualified id, so a diff of two releases' catalogues
89
+ * lists every added and removed name. The operations follow the order of the installed plugins.
90
+ * @param product - The resolved product.
91
+ * @param api - The catalogue plugin's api, which the descriptions are translated from.
92
+ * @throws {@link Error} When a catalogue cannot be read for a description, naming the plugin and
93
+ * the language.
94
+ */
95
+ function cataloguesFor(product, api) {
96
+ const describe = describerOf(api);
97
+ const identity = {
98
+ id: product.productId,
99
+ version: product.version
100
+ };
101
+ return {
102
+ access: {
103
+ entitlements: product.entitlements.map((one) => entryOf$1(one, describe)).toSorted(byId),
104
+ permissions: product.permissions.map((one) => ({
105
+ ...entryOf$1(one, describe),
106
+ resource: one.resource
107
+ })).toSorted(byId),
108
+ product: identity,
109
+ resources: product.resources.map((one) => entryOf$1(one, describe)).toSorted(byId),
110
+ roles: product.roles.map((one) => ({
111
+ ...entryOf$1(one, describe),
112
+ permissions: one.permissions
113
+ })).toSorted(byId)
114
+ },
115
+ flags: {
116
+ flags: product.flags.map((one) => ({
117
+ ...entryOf$1(one, describe),
118
+ default: one.default,
119
+ expires: one.expires,
120
+ kind: one.kind,
121
+ product: one.product,
122
+ variants: one.variants
123
+ })).toSorted(byId),
124
+ product: identity
125
+ },
126
+ operations: {
127
+ operations: product.plugins.flatMap(({ id }) => [...product.queries.filter(({ plugin }) => plugin === id).map((one) => operationOf(one, "query")), ...product.mutations.filter(({ plugin }) => plugin === id).map((one) => operationOf(one, "mutation"))]),
128
+ product: identity
129
+ }
130
+ };
131
+ }
132
+ //#endregion
133
+ //#region src/chunks.ts
134
+ /**
135
+ * Names the chunk each lazy module of an installed plugin's web package builds into: one chunk per
136
+ * plugin, which every page, extension, command and settings section of the plugin shares.
137
+ *
138
+ * @remarks
139
+ * The product imports each web package's main entry statically, through its definition. The main
140
+ * entry and every module it imports statically build into the chunks the entry loads, because in
141
+ * the plugin's chunk they would make the first load fetch the whole plugin.
142
+ */
143
+ /**
144
+ * Returns true where an entry of the build imports a module statically, directly or through other
145
+ * modules.
146
+ *
147
+ * @remarks
148
+ * The walk follows each module's static importers back towards the entries. A dynamic import
149
+ * ends the walk, because a module behind one loads after the entry.
150
+ */
151
+ function initial(id, chunking) {
152
+ const seen = /* @__PURE__ */ new Set();
153
+ const queue = [id];
154
+ for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
155
+ const info = seen.has(next) ? null : chunking.getModuleInfo(next);
156
+ seen.add(next);
157
+ if (info?.isEntry === true) return true;
158
+ queue.push(...info?.importers ?? []);
159
+ }
160
+ return false;
161
+ }
162
+ /**
163
+ * Returns the chunk a module builds into: `plugin-<id>` for a module of an installed plugin's web
164
+ * package that no entry imports statically, and null for every other module.
165
+ *
166
+ * @param id - The module's id, its absolute path.
167
+ * @param chunking - The context rolldown passes a group's name function.
168
+ * @param packages - Each installed plugin's web package, by plugin id.
169
+ */
170
+ function pluginChunkOf(id, chunking, packages) {
171
+ const owner = Object.entries(packages).find(([, { directory }]) => id.startsWith(`${directory}${sep}`));
172
+ return owner === void 0 || initial(id, chunking) ? null : `plugin-${owner[0]}`;
173
+ }
174
+ //#endregion
175
+ //#region src/discover.ts
176
+ /**
177
+ * Finds each installed plugin's web package and contract package among the modules the product's
178
+ * definition loaded.
179
+ *
180
+ * @remarks
181
+ * The definition imports every installed plugin's manifest, and each manifest names its contract,
182
+ * so the modules its evaluation loaded already contain both. A package is the one that contains
183
+ * the module defining the object, matched by identity. A manifest the definition builds itself,
184
+ * which no module exports, belongs to the definition module's package. No package is imported for
185
+ * the search alone, so no component package and no React is loaded in Node.
186
+ */
187
+ /**
188
+ * Returns true where a value is an object whose members can be read by name.
189
+ */
190
+ function isRecord(value) {
191
+ return typeof value === "object" && value !== null;
192
+ }
193
+ /**
194
+ * Returns each installed plugin the definition states with a readable manifest and contract.
195
+ *
196
+ * @remarks
197
+ * A definition of another shape installs nothing here, and the resolver's shape check reports it.
198
+ * @param definition - The definition's default export.
199
+ */
200
+ function installationsOf(definition) {
201
+ const plugins = Reflect.get(definition, "plugins");
202
+ return (Array.isArray(plugins) ? plugins : []).flatMap((plugin) => {
203
+ const manifest = isRecord(plugin) ? plugin["manifest"] : void 0;
204
+ const contract = isRecord(manifest) ? manifest["contract"] : void 0;
205
+ const pluginId = isRecord(contract) ? contract["pluginId"] : void 0;
206
+ return isRecord(manifest) && isRecord(contract) && typeof pluginId === "string" ? [{
207
+ contract,
208
+ manifest,
209
+ pluginId
210
+ }] : [];
211
+ });
212
+ }
213
+ /**
214
+ * Returns the directory of the package that defines each object a loaded module exports.
215
+ *
216
+ * @remarks
217
+ * A module evaluates after every module it imports, and a module that re-exports an object
218
+ * imports the module that defines it. The first module that exports an object, in evaluation
219
+ * order, is therefore the one that defines it.
220
+ * @param loaded - Every module the definition's evaluation loaded, in evaluation order.
221
+ */
222
+ function ownersOf(loaded) {
223
+ const owners = /* @__PURE__ */ new Map();
224
+ for (const { exports, file } of loaded) {
225
+ const directory = owning(file);
226
+ for (const value of Object.values(exports)) if (directory !== void 0 && isRecord(value) && !owners.has(value)) owners.set(value, directory);
227
+ }
228
+ return owners;
229
+ }
230
+ /**
231
+ * Returns the package at a directory, or undefined where its manifest states no name.
232
+ */
233
+ function packageIn(directory) {
234
+ const manifest = directory === void 0 ? void 0 : manifestAt(directory);
235
+ const name = manifest === void 0 ? void 0 : text(manifest, "name");
236
+ return directory === void 0 || name === void 0 ? void 0 : {
237
+ directory,
238
+ name
239
+ };
240
+ }
241
+ /**
242
+ * Finds each installed plugin's web package and contract package.
243
+ *
244
+ * @remarks
245
+ * A web package outside the product's own package and its dependencies is left out, so the
246
+ * resolver reports the plugin. A contract package is recorded wherever it is, because the build
247
+ * reads it to leave the plugin's own catalogue out of the namespaces another package publishes.
248
+ * @param definition - The definition's default export.
249
+ * @param loaded - Every module the definition's evaluation loaded, in evaluation order.
250
+ * @param root - The product's directory, whose dependencies are walked.
251
+ * @param file - The definition module, whose package a manifest the definition builds belongs to.
252
+ */
253
+ function discover(definition, loaded, root, file) {
254
+ const owners = ownersOf(loaded);
255
+ const own = owning(realpathSync(file));
256
+ const depended = /* @__PURE__ */ new Set([realpathSync(root), ...dependencies(root).map(({ at }) => at)]);
257
+ const contracts = {};
258
+ const packages = {};
259
+ for (const { contract, manifest, pluginId } of installationsOf(definition)) {
260
+ const web = packageIn(owners.get(manifest) ?? own);
261
+ const declaring = packageIn(owners.get(contract));
262
+ if (web !== void 0 && depended.has(web.directory)) packages[pluginId] = web;
263
+ if (declaring !== void 0) contracts[pluginId] = declaring;
264
+ }
265
+ return {
266
+ contracts,
267
+ packages
268
+ };
269
+ }
270
+ //#endregion
271
+ //#region src/words.ts
272
+ /**
273
+ * Builds the words the resolver checks from the catalogue plugin's api, so the build checks the
274
+ * words the page loads, found under the scopes the catalogue plugin follows.
275
+ */
276
+ /**
277
+ * Returns the fallback catalogue of each namespace, and the packages that publish each namespace
278
+ * other than the plugin's contract package.
279
+ *
280
+ * @remarks
281
+ * The resolver reports a plugin whose namespace has no entry in `catalogues`. The application's
282
+ * own catalogues are left out of the publishers, because an application may change the words of
283
+ * any package it installs.
284
+ * @param api - The catalogue plugin's api.
285
+ * @param contracts - Each installed plugin's contract package, by plugin id.
286
+ */
287
+ function wordsOf(api, contracts) {
288
+ const catalogues = api.catalogues();
289
+ const fallback = new Set(catalogues.filter(({ language }) => language === api.fallback).map(({ namespace }) => namespace));
290
+ const publishers = /* @__PURE__ */ new Map();
291
+ for (const { namespace, own, owner } of catalogues) if (!own && owner !== contracts[namespace]?.name) publishers.set(namespace, /* @__PURE__ */ new Set([...publishers.get(namespace) ?? [], owner]));
292
+ return {
293
+ catalogues: Object.fromEntries([...fallback].map((namespace) => [namespace, api.words(api.fallback, namespace)])),
294
+ namespaces: Object.fromEntries([...publishers].map(([namespace, owners]) => [namespace, [...owners]]))
295
+ };
296
+ }
297
+ /**
298
+ * Returns the line that tells a product how the catalogue plugin finds a package's catalogues.
299
+ *
300
+ * @remarks
301
+ * The catalogue plugin follows the packages of the scopes it was given alone, so a package
302
+ * without a scope is never followed.
303
+ * @param name - Name of the package whose catalogues the catalogue plugin did not find.
304
+ */
305
+ function hintOf(name) {
306
+ const [scope] = name.split("/");
307
+ return name.startsWith("@") ? `${name} has catalogues the i18n layer does not follow. Add ${scope} to the scopes of the i18n layer.` : `${name} has catalogues the i18n layer does not follow, because the layer follows scoped packages alone.`;
308
+ }
309
+ /**
310
+ * Returns one line per installed plugin whose contract package has catalogues the catalogue plugin
311
+ * did not find, naming the scope the product adds to the i18n layer.
312
+ *
313
+ * @param api - The catalogue plugin's api.
314
+ * @param contracts - Each installed plugin's contract package, by plugin id.
315
+ */
316
+ function hintsOf(api, contracts) {
317
+ const found = new Set(api.catalogues().map(({ owner }) => owner));
318
+ return Object.values(contracts).filter(({ directory, name }) => !found.has(name) && existsSync(join(directory, LOCALES))).map(({ name }) => hintOf(name));
319
+ }
320
+ //#endregion
321
+ //#region src/compose.ts
322
+ /**
323
+ * Composes a product: imports its definition through Vite, finds its plugins' packages, and
324
+ * resolves it with the words the catalogue plugin found.
325
+ */
326
+ /**
327
+ * Returns the message of an error, or the value written as text.
328
+ */
329
+ function messageOf(error) {
330
+ return error instanceof Error ? error.message : String(error);
331
+ }
332
+ /**
333
+ * Imports the definition module through the importer.
334
+ *
335
+ * @throws {@link Error} When the module or anything it imports fails to load in Node, with the rule
336
+ * a web package's main entry follows.
337
+ */
338
+ async function evaluated(through, file, definition) {
339
+ try {
340
+ return await through.import(file);
341
+ } catch (error) {
342
+ throw new Error(`${definition} failed to load in Node: ${messageOf(error)}. The build evaluates the definition, every contract and every manifest entry in Node. A web package's main entry imports its components and React through lazy importers alone.`, { cause: error });
343
+ }
344
+ }
345
+ /**
346
+ * Returns the definition a module exports by default.
347
+ *
348
+ * @throws {@link Error} When the module has no default export that is an object.
349
+ */
350
+ function definitionOf$1(module, definition) {
351
+ const exported = module["default"];
352
+ if (typeof exported !== "object" || exported === null) throw new Error(`${definition} has no default export, and the build reads the product's definition from it.`);
353
+ return exported;
354
+ }
355
+ /**
356
+ * Composes a product from its definition.
357
+ *
358
+ * @remarks
359
+ * The importer resolves every package through Vite under the product's conditions. The resolver
360
+ * checks the shape of every member of the definition before it reads one.
361
+ * @param composing - The definition's path, the project root, the importer and the catalogue
362
+ * plugin's api.
363
+ * @returns The packages found, the files read, the hints and the resolution.
364
+ * @throws {@link Error} When the definition fails to load, or has no default export.
365
+ */
366
+ async function compose(composing) {
367
+ const { definition, root, through, words } = composing;
368
+ const file = join(root, definition);
369
+ const imported = await evaluated(through, file, definition);
370
+ const stated = definitionOf$1(imported.module, definition);
371
+ const discovery = discover(stated, imported.loaded, root, file);
372
+ const resolution = resolveProduct(stated, discovery.packages, {
373
+ ...wordsOf(words, discovery.contracts),
374
+ validateHotkey
375
+ });
376
+ return {
377
+ discovery,
378
+ files: imported.files,
379
+ hints: hintsOf(words, discovery.contracts),
380
+ resolution
381
+ };
382
+ }
383
+ //#endregion
384
+ //#region src/module.ts
385
+ /**
386
+ * The specifier an application imports the product from.
387
+ */
388
+ const ID = "virtual:product";
389
+ /**
390
+ * The resolved id of the module, behind the NUL that marks it as generated.
391
+ */
392
+ const RESOLVED = `\0${ID}`;
393
+ /**
394
+ * Returns the source of `virtual:product`.
395
+ *
396
+ * @remarks
397
+ * The module imports the definition. The page receives each manifest from it, keyed by plugin id,
398
+ * with its lazy importers and its contract's search validators. Every member the build resolved
399
+ * is a literal in the module, and the page runs no check of its own.
400
+ * @param definition - The definition module's path from the project root, such as
401
+ * `/src/product.ts`.
402
+ * @param product - The resolved product.
403
+ * @throws {@link Error} When a member of the product cannot be written as source, naming its path.
404
+ */
405
+ function moduleOf(definition, product) {
406
+ return [
407
+ `import definition from ${quoted(definition)};`,
408
+ "",
409
+ `const resolved = ${literal(product, "product")};`,
410
+ "",
411
+ "export const product = {",
412
+ " ...resolved,",
413
+ " manifests: Object.fromEntries(",
414
+ " definition.plugins.map(({ manifest }) => [manifest.contract.pluginId, manifest]),",
415
+ " ),",
416
+ "};",
417
+ ""
418
+ ].join("\n");
419
+ }
420
+ //#endregion
421
+ //#region src/standalone.ts
422
+ /**
423
+ * Writes the standalone page of a plugin: the definition of the product that runs the plugin, the
424
+ * page's document and the page's entry.
425
+ */
426
+ /**
427
+ * Path of the generated definition, from the project root.
428
+ */
429
+ const GENERATED = "node_modules/.stealth/standalone/product.ts";
430
+ /**
431
+ * The specifier the page imports the generated definition by.
432
+ */
433
+ const PRODUCT = "virtual:standalone-product";
434
+ /**
435
+ * Path of the manifest module where the page states none.
436
+ */
437
+ const MANIFEST = "src/manifest.ts";
438
+ /**
439
+ * The page's document, from the project root.
440
+ */
441
+ const DOCUMENT = "index.html";
442
+ /**
443
+ * The specifier of the page's entry.
444
+ */
445
+ const ENTRY = "virtual:standalone";
446
+ /**
447
+ * The resolved id of the page's entry, behind the NUL that marks it as generated.
448
+ */
449
+ const RESOLVED_ENTRY = `\0${ENTRY}`;
450
+ /**
451
+ * The resolved id of the definition the page imports, behind the NUL that marks it as generated.
452
+ */
453
+ const RESOLVED_PRODUCT = `\0${PRODUCT}`;
454
+ /**
455
+ * The resolved id of each module the page imports by a specifier of its own.
456
+ */
457
+ const SPECIFIED = /* @__PURE__ */ new Map([
458
+ [`/@id/${ENTRY}`, RESOLVED_ENTRY],
459
+ [ENTRY, RESOLVED_ENTRY],
460
+ [PRODUCT, RESOLVED_PRODUCT]
461
+ ]);
462
+ /**
463
+ * The language of the document where the page states no locale.
464
+ */
465
+ const LANGUAGE = "en-US";
466
+ /**
467
+ * Returns the source of a definition that passes the plugin's manifest module and each contract
468
+ * package beside it to `standaloneFrom`.
469
+ *
470
+ * @param from - The specifier the definition imports the manifest module by.
471
+ * @param page - The manifest module and the contract packages beside the plugin.
472
+ */
473
+ function sourceOf(from, page) {
474
+ const beside = page.beside ?? [];
475
+ return [
476
+ "import { standaloneFrom } from \"@stealthscale/sdk-host/standalone\";",
477
+ "",
478
+ `import * as plugin from ${quoted(from)};`,
479
+ ...beside.map((name, index) => `import * as beside${String(index)} from ${quoted(name)};`),
480
+ "",
481
+ "export default standaloneFrom(",
482
+ ` { from: ${quoted(page.manifest ?? MANIFEST)}, module: plugin },`,
483
+ " [",
484
+ ...beside.map((name, index) => ` { from: ${quoted(name)}, module: beside${String(index)} },`),
485
+ " ],",
486
+ ");",
487
+ ""
488
+ ].join("\n");
489
+ }
490
+ /**
491
+ * Returns the source of the generated definition, which the build imports in Node.
492
+ *
493
+ * @remarks
494
+ * The definition imports the manifest module by a path relative to its own directory, three
495
+ * levels under the project root, so the path starts with `../`.
496
+ * @param root - The project root.
497
+ * @param page - The manifest module and the contract packages beside the plugin.
498
+ */
499
+ function definitionOf(root, page) {
500
+ const manifest = join(root, page.manifest ?? MANIFEST);
501
+ return sourceOf(normalizePath(relative(dirname(join(root, GENERATED)), manifest)), page);
502
+ }
503
+ /**
504
+ * Writes the generated definition under the project root, and leaves a file with the same source
505
+ * untouched.
506
+ *
507
+ * @remarks
508
+ * A file left untouched keeps its time of change, so the watcher reports no change and the
509
+ * product is not composed again.
510
+ * @param root - The project root.
511
+ * @param page - The manifest module and the contract packages beside the plugin.
512
+ */
513
+ function generate(root, page) {
514
+ const file = join(root, GENERATED);
515
+ const source = definitionOf(root, page);
516
+ if (existsSync(file) && readFileSync(file, "utf8") === source) return;
517
+ mkdirSync(dirname(file), { recursive: true });
518
+ writeFileSync(file, source);
519
+ }
520
+ /**
521
+ * Returns the source of the page's document: an element with the id `root`, and the entry.
522
+ */
523
+ function documentOf(page) {
524
+ const [language = LANGUAGE] = page.locales ?? [];
525
+ return [
526
+ "<!doctype html>",
527
+ `<html lang=${quoted(language)}>`,
528
+ " <head>",
529
+ " <meta charset=\"utf-8\" />",
530
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />",
531
+ " <title>Standalone page</title>",
532
+ " </head>",
533
+ " <body>",
534
+ " <div id=\"root\"></div>",
535
+ ` <script type="module" src="/@id/${ENTRY}"><\/script>`,
536
+ " </body>",
537
+ "</html>",
538
+ ""
539
+ ].join("\n");
540
+ }
541
+ /**
542
+ * Returns the source of the page's entry, which renders the page over `virtual:product` and
543
+ * `virtual:i18n`.
544
+ *
545
+ * @remarks
546
+ * The entry imports the stylesheet alone, and the page's modules through `import()`. The bundled
547
+ * dev server installs the React refresh runtime in the entry's own body, and a chunk the entry
548
+ * imports statically runs before that body, so a component in a chunk the bundler shares between
549
+ * the entry and a lazy module would run before the runtime and throw. Behind `import()`, every
550
+ * component runs after it. The entry awaits nothing at its top level, because a module with a
551
+ * top-level await makes the bundler move every module it shares with a lazy module into a chunk
552
+ * of its own.
553
+ */
554
+ function entryOf(page) {
555
+ const { glyphs, locales, themes } = page;
556
+ const members = [
557
+ "catalogues",
558
+ ...glyphs === void 0 ? [] : ["glyphs"],
559
+ ...locales === void 0 ? [] : [`locales: ${literal(locales, "standalone.locales")}`],
560
+ "product",
561
+ ...themes === void 0 ? [] : [`themes: ${literal(themes, "standalone.themes")}`]
562
+ ];
563
+ const modules = [
564
+ ["{ renderStandalone }", quoted("@stealthscale/sdk-host/standalone/app")],
565
+ ["{ catalogues }", quoted("virtual:i18n")],
566
+ ["{ product }", quoted("virtual:product")],
567
+ ...glyphs === void 0 ? [] : [["{ glyphs }", quoted(posix.join("/", normalizePath(glyphs)))]]
568
+ ];
569
+ return [
570
+ "import \"@stealthscale/theme/styles.css\";",
571
+ "",
572
+ "async function start() {",
573
+ ` const [${modules.map(([names]) => names).join(", ")}] = await Promise.all([`,
574
+ ...modules.map(([, from]) => ` import(${String(from)}),`),
575
+ " ]);",
576
+ "",
577
+ ` await renderStandalone({ ${members.join(", ")} });`,
578
+ "}",
579
+ "",
580
+ "void start();",
581
+ ""
582
+ ].join("\n");
583
+ }
584
+ /**
585
+ * Returns the id the page's document, its entry or its definition resolves to, and undefined for
586
+ * any other specifier.
587
+ *
588
+ * @param root - The project root.
589
+ * @param id - The specifier.
590
+ */
591
+ function pageIdOf(root, id) {
592
+ const document = normalizePath(join(root, DOCUMENT));
593
+ if (id === DOCUMENT || id === `/${DOCUMENT}` || id === document) return document;
594
+ return SPECIFIED.get(id);
595
+ }
596
+ /**
597
+ * Returns the source of the page's document, its entry or its definition, and undefined for any
598
+ * other id.
599
+ *
600
+ * @remarks
601
+ * The definition the page imports is the generated definition with its manifest module imported
602
+ * from the project root. An application's chunks place every module under `node_modules` in the
603
+ * vendor chunk, and the generated file there would import the entry chunk that imports it, so the
604
+ * definition would run before the manifest module.
605
+ * @param root - The project root.
606
+ * @param page - The page's manifest module, contracts, locales, themes and glyphs.
607
+ * @param id - The resolved id.
608
+ */
609
+ function pageSourceOf(root, page, id) {
610
+ if (id === normalizePath(join(root, DOCUMENT))) return documentOf(page);
611
+ if (id === RESOLVED_ENTRY) return entryOf(page);
612
+ return id === RESOLVED_PRODUCT ? sourceOf(posix.join("/", normalizePath(page.manifest ?? MANIFEST)), page) : void 0;
613
+ }
614
+ //#endregion
615
+ //#region src/plugin.ts
616
+ /**
617
+ * The Vite plugin that composes a product from its plugins while the product builds, serves the
618
+ * result as `virtual:product`, and writes the catalogues the product's services read. Under the
619
+ * `standalone` option it serves a plugin's standalone page as well.
620
+ */
621
+ /**
622
+ * Directory of the build's output the catalogues are written to.
623
+ */
624
+ const CATALOGUES = ".product";
625
+ /**
626
+ * Path of the definition module where the options state none.
627
+ */
628
+ const DEFINITION = "src/product.ts";
629
+ /**
630
+ * Priority of the plugin chunks' group: above the shared chunk's 3, below the vendor chunk's 5.
631
+ */
632
+ const PRIORITY = 4;
633
+ /**
634
+ * Returns the message a product that does not resolve fails with: every problem, then the hints.
635
+ */
636
+ function failureOf({ hints, resolution }) {
637
+ return [
638
+ "The product does not resolve:",
639
+ ...resolution.problems.map(lineOf),
640
+ ...hints
641
+ ].join("\n");
642
+ }
643
+ /**
644
+ * Composes the product through the plugin's importer, after dropping the transforms of every file
645
+ * that changed since the composition before.
646
+ *
647
+ * @remarks
648
+ * The first composition opens the importer, and every later one reuses its environment. Vite
649
+ * keeps a closed environment reachable, so an environment per composition would leave every
650
+ * module it transformed on the heap.
651
+ */
652
+ async function composedThrough(state, definition, words) {
653
+ state.through ??= importer({
654
+ conditions: state.conditions,
655
+ root: state.root
656
+ });
657
+ const through = await state.through;
658
+ through.invalidate([...state.changed]);
659
+ state.changed.clear();
660
+ return compose({
661
+ definition,
662
+ root: state.root,
663
+ through,
664
+ words
665
+ });
666
+ }
667
+ /**
668
+ * Starts the product's composition once, and returns the one started before on a later call.
669
+ *
670
+ * @throws {@link Error} When the configuration has no catalogue plugin, whose words the
671
+ * composition checks.
672
+ */
673
+ function composed(state, definition) {
674
+ const { words } = state;
675
+ if (words === void 0) throw new Error("The configuration has no stealth:i18n plugin, whose catalogues the product plugin checks. Add the layers of @stealthscale/vite-config-i18n.");
676
+ state.composing ??= composedThrough(state, definition, words);
677
+ return state.composing;
678
+ }
679
+ /**
680
+ * Closes the importer the compositions share and forgets it, so a later composition opens a new
681
+ * one.
682
+ */
683
+ async function closed(state) {
684
+ const { through } = state;
685
+ state.through = void 0;
686
+ await (await through)?.close();
687
+ }
688
+ /**
689
+ * Returns the configuration whose group builds each installed plugin's lazy modules into a chunk
690
+ * of its own.
691
+ *
692
+ * @remarks
693
+ * The group names a chunk from the web packages the composition found. A build start records
694
+ * them before the bundler forms its chunks.
695
+ */
696
+ function chunked(state) {
697
+ return { build: { rolldownOptions: { output: { codeSplitting: { groups: [{
698
+ debugName: "plugins",
699
+ name: (id, chunking) => pluginChunkOf(id, chunking, state.packages),
700
+ priority: PRIORITY
701
+ }] } } } } };
702
+ }
703
+ /**
704
+ * Returns the catalogue files of a client build, and none under a dev server or for another
705
+ * environment.
706
+ *
707
+ * @param state - The command, the resolved product and the catalogue plugin's api.
708
+ * @param consumer - The consumer of the environment whose bundle is written.
709
+ * @throws {@link Error} When a catalogue cannot be read for a description.
710
+ */
711
+ function catalogueFiles(state, consumer) {
712
+ const { command, product: resolved, words } = state;
713
+ if (consumer !== "client" || command !== "build" || resolved === void 0 || words === void 0) return [];
714
+ return Object.entries(cataloguesFor(resolved, words)).map(([name, catalogue]) => ({
715
+ fileName: `${CATALOGUES}/${name}.json`,
716
+ source: `${JSON.stringify(catalogue, null, 2)}\n`,
717
+ type: "asset"
718
+ }));
719
+ }
720
+ /**
721
+ * Records a composition: its files, its web packages, its problems, and its product where it
722
+ * resolved, so a composition with problems keeps the last product that resolved.
723
+ *
724
+ * @param state - The plugin's state, which the composition replaces.
725
+ * @param composition - The files read, the web packages found and the resolution.
726
+ * @param warn - Prints one warning of the resolution.
727
+ */
728
+ function applied(state, composition, warn) {
729
+ const { problems, product: resolved, warnings } = composition.resolution;
730
+ for (const warning of warnings) warn(lineOf(warning));
731
+ state.files = new Set(composition.files.map((file) => normalizePath(file)));
732
+ state.packages = composition.discovery.packages;
733
+ state.product = resolved ?? state.product;
734
+ state.failure = problems.length === 0 ? void 0 : failureOf(composition);
735
+ }
736
+ /**
737
+ * Returns what the page receives after a change: a reload where the product resolved, and the
738
+ * problems in the error overlay where it did not.
739
+ */
740
+ function noticeOf(state) {
741
+ return state.failure === void 0 ? {
742
+ path: "*",
743
+ type: "full-reload"
744
+ } : {
745
+ err: {
746
+ message: state.failure,
747
+ stack: ""
748
+ },
749
+ type: "error"
750
+ };
751
+ }
752
+ /**
753
+ * Composes the product again and records the composition, or its load error as the failure, unless
754
+ * a later change superseded the change.
755
+ *
756
+ * @param state - The plugin's state.
757
+ * @param definition - The definition's path from the project root.
758
+ * @param context - The watch hook's context, whose bundler prints the warnings.
759
+ * @param change - The number of the change the composition is for.
760
+ */
761
+ async function recorded(state, definition, context, change) {
762
+ if (change !== state.changes) return;
763
+ try {
764
+ applied(state, await composed(state, definition), (message) => {
765
+ context.warn(message);
766
+ });
767
+ } catch (error) {
768
+ state.failure = error.message;
769
+ }
770
+ }
771
+ /**
772
+ * Follows a change to a file the last composition read.
773
+ *
774
+ * @remarks
775
+ * A watching build composes again at its next build start. A dev server runs one composition at
776
+ * a time, in the order of the changes. It skips the composition of a change that a later change
777
+ * superseded before its turn. The composition recorded last starts after the last change. A
778
+ * composition that fails to load records its error as the failure and keeps the last product. A
779
+ * server that bundles runs no hot update, so the page receives the notice here.
780
+ * @param state - The plugin's state.
781
+ * @param definition - The definition's path from the project root.
782
+ * @param context - The watch hook's context.
783
+ */
784
+ async function followed(state, definition, context) {
785
+ state.composing = void 0;
786
+ if (state.command === "build") return;
787
+ state.changes += 1;
788
+ const change = state.changes;
789
+ state.following = state.following.then(() => recorded(state, definition, context, change));
790
+ await state.following;
791
+ const { config, hot } = context.environment;
792
+ if (config.isBundled && hot !== void 0) hot.send(noticeOf(state));
793
+ }
794
+ /**
795
+ * Brings a page served one module per file up to date after a change to a file the last
796
+ * composition read: invalidates `virtual:product` in the environment, and sends the client's
797
+ * page a notice.
798
+ *
799
+ * @returns An empty list, so the server handles the change no further.
800
+ */
801
+ function updated(state, context) {
802
+ const { hot, moduleGraph, name } = context.environment;
803
+ const node = moduleGraph.getModuleById(RESOLVED);
804
+ if (node !== void 0) moduleGraph.invalidateModule(node);
805
+ if (name === "client") hot.send(noticeOf(state));
806
+ return [];
807
+ }
808
+ /**
809
+ * Returns the specifier `virtual:product` imports the definition by: `virtual:standalone-product`
810
+ * for a definition the plugin generates, and the definition's path from the project root for any
811
+ * other.
812
+ *
813
+ * @param state - The project root.
814
+ * @param definition - The definition's path from the project root.
815
+ * @param generated - Whether the plugin generates the definition of a standalone page.
816
+ */
817
+ function importedOf(state, definition, generated) {
818
+ return generated ? PRODUCT : `/${normalizePath(relative(state.root, resolve(state.root, definition)))}`;
819
+ }
820
+ /**
821
+ * Returns the source of `virtual:product`, which depends on every file the composition read.
822
+ *
823
+ * @param state - The resolved product and the files the composition read.
824
+ * @param imported - The specifier the module imports the definition by.
825
+ * @param context - The load hook's context, which watches the files.
826
+ * @throws {@link Error} When the product did not resolve, with every problem.
827
+ */
828
+ function served(state, imported, context) {
829
+ if (state.product === void 0) throw new Error(state.failure ?? "The product has not been composed yet.");
830
+ for (const file of state.files) context.addWatchFile(file);
831
+ return moduleOf(imported, state.product);
832
+ }
833
+ /**
834
+ * Creates the plugin that composes a product from its plugins, serves `virtual:product`, builds
835
+ * each plugin's lazy modules into one chunk, and writes the access, flag and operation catalogues
836
+ * of a build.
837
+ *
838
+ * @remarks
839
+ * The product is composed when the build starts, and when a dev server starts. A build fails on
840
+ * a problem and prints each warning. A dev server serves no product until the definition
841
+ * resolves, and `virtual:product` then fails with the problems. A change to a file the
842
+ * composition read composes the product again: the page reloads, or shows the problems in the
843
+ * error overlay and keeps the last product. The client build writes the catalogues under
844
+ * `.product` in its output. Under `standalone`, the plugin writes the definition of the product
845
+ * that runs the plugin at `node_modules/.stealth/standalone/product.ts` once the configuration
846
+ * resolves, unless `definition` names one. It serves the page's document as `index.html`, its
847
+ * entry as `virtual:standalone`, and the generated definition as `virtual:standalone-product`.
848
+ * @param options - The definition's path, and the standalone page.
849
+ */
850
+ function product(options = {}) {
851
+ const state = {
852
+ changed: /* @__PURE__ */ new Set(),
853
+ changes: 0,
854
+ command: "serve",
855
+ files: /* @__PURE__ */ new Set(),
856
+ following: Promise.resolve(),
857
+ packages: {},
858
+ root: process.cwd()
859
+ };
860
+ const page = options.standalone;
861
+ const generated = page !== void 0 && options.definition === void 0;
862
+ const definition = options.definition ?? (generated ? "node_modules/.stealth/standalone/product.ts" : DEFINITION);
863
+ return {
864
+ /**
865
+ * Composes the product, and reports its warnings and its problems.
866
+ *
867
+ * @throws {@link Error} When the build's product does not resolve.
868
+ */
869
+ async buildStart() {
870
+ applied(state, await composed(state, definition), (message) => {
871
+ this.warn(message);
872
+ });
873
+ for (const file of state.files) this.addWatchFile(file);
874
+ if (state.failure !== void 0 && state.command === "build") throw new Error(state.failure);
875
+ },
876
+ /**
877
+ * Closes the importer the compositions share, at the end of a build and when a dev server
878
+ * closes.
879
+ */
880
+ closeBundle() {
881
+ return closed(state);
882
+ },
883
+ /**
884
+ * Adds the group that builds each installed plugin's lazy modules into a chunk of its own.
885
+ */
886
+ config() {
887
+ return chunked(state);
888
+ },
889
+ /**
890
+ * Records the project root, the command, the conditions and the catalogue plugin's api, and
891
+ * writes the generated definition of a standalone page.
892
+ */
893
+ configResolved(config) {
894
+ state.command = config.command;
895
+ state.conditions = config.ssr.resolve?.conditions;
896
+ state.root = config.root;
897
+ state.words = cataloguesOf(config.plugins);
898
+ if (page !== void 0 && options.definition === void 0) generate(config.root, page);
899
+ },
900
+ /**
901
+ * Writes the access, flag and operation catalogues into the client build's output, and nothing
902
+ * under a dev server or for another environment.
903
+ *
904
+ * @throws {@link Error} When a catalogue cannot be read for a description.
905
+ */
906
+ generateBundle() {
907
+ for (const file of catalogueFiles(state, this.environment.config.consumer)) this.emitFile(file);
908
+ },
909
+ /**
910
+ * Brings a page served one module per file up to date after a change to a file the last
911
+ * composition read, and leaves every other change to the server.
912
+ */
913
+ hotUpdate({ file }) {
914
+ return state.files.has(normalizePath(file)) ? updated(state, this) : void 0;
915
+ },
916
+ /**
917
+ * Serves `virtual:product`, and the standalone page's document, entry and definition.
918
+ *
919
+ * @throws {@link Error} When the product did not resolve, with every problem.
920
+ */
921
+ load(id) {
922
+ if (id === RESOLVED) return served(state, importedOf(state, definition, generated), this);
923
+ return page === void 0 ? void 0 : pageSourceOf(state.root, page, id);
924
+ },
925
+ name: "stealth:product",
926
+ /**
927
+ * Claims `virtual:product`, and the standalone page's document, entry and definition, leaving
928
+ * every other import alone.
929
+ */
930
+ resolveId(id) {
931
+ if (id === "virtual:product") return RESOLVED;
932
+ return page === void 0 ? void 0 : pageIdOf(state.root, id);
933
+ },
934
+ /**
935
+ * Follows a change to a file the last composition read, which the next composition transforms
936
+ * again, and leaves every other change alone.
937
+ */
938
+ async watchChange(id) {
939
+ const file = normalizePath(id);
940
+ if (!state.files.has(file)) return;
941
+ state.changed.add(file);
942
+ await followed(state, definition, this);
943
+ }
944
+ };
945
+ }
946
+ //#endregion
947
+ export { ID, product };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@stealthscale/vite-plugin-product",
3
+ "version": "0.1.0",
4
+ "description": "Composes a product from its plugins while it builds: checks every plugin against the others, resolves the product, and serves it as virtual:product.",
5
+ "keywords": [
6
+ "plugins",
7
+ "vite-plugin"
8
+ ],
9
+ "homepage": "https://github.com/stealth-scale/scale/tree/main/packages/vite-plugin-product#readme",
10
+ "bugs": "https://github.com/stealth-scale/scale/issues",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/stealth-scale/scale.git",
15
+ "directory": "packages/vite-plugin-product"
16
+ },
17
+ "files": [
18
+ "client.d.ts",
19
+ "dist",
20
+ "LICENSE",
21
+ "README.md"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "imports": {
26
+ "#*": "./src/*"
27
+ },
28
+ "exports": {
29
+ ".": "./dist/index.mjs",
30
+ "./package.json": "./package.json",
31
+ "./client": "./client.d.ts"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@tanstack/hotkeys": "0.10.0"
38
+ },
39
+ "devDependencies": {
40
+ "@arethetypeswrong/core": "^0.18",
41
+ "@stealthscale/sdk-core": "^0.1.0",
42
+ "@stealthscale/testing": "^0.3.0",
43
+ "@stealthscale/testing-config": "^0.5.0",
44
+ "@stealthscale/vite-config": "^0.6.0",
45
+ "@stealthscale/vite-config-typescript": "^0.3.0",
46
+ "@stealthscale/vite-plugin-base": "^0.2.0",
47
+ "@stealthscale/vite-plugin-i18n": "^0.1.0",
48
+ "@types/node": "^26.6",
49
+ "@vitest/coverage-v8": "5.0.1",
50
+ "publint": "^0.3",
51
+ "typescript": "^7.0",
52
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
53
+ "vitest": "5.0.1"
54
+ },
55
+ "peerDependencies": {
56
+ "@stealthscale/sdk-core": "^0.1.0",
57
+ "@stealthscale/vite-plugin-base": "^0.2.0",
58
+ "@stealthscale/vite-plugin-i18n": "^0.1.0",
59
+ "vite": "^8.3"
60
+ },
61
+ "engines": {
62
+ "node": ">=26.0.0"
63
+ },
64
+ "scripts": {
65
+ "build": "vp pack",
66
+ "check": "vp check",
67
+ "test": "vp test"
68
+ }
69
+ }