@pitlane/assets 0.0.0-stage → 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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @pitlane/assets
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3053134: New package: framework-neutral asset resolution for server-rendered apps.
8
+
9
+ `createAssetResolver(manifest)` resolves source paths such as `app/entry.browser.ts` or `file:app/counter.tsx#Counter` to the URLs the current Vite dev server or build produced. Its `getScriptEntry`, `getHref`, `getPreloads`, `getStylesheets`, and `getImportMap` methods return script URLs, `modulepreload` hints, stylesheet lists, and the client import map. The resolver matches the asset server from `remix/assets`, so it can be passed to Remix's `render({ assets })`, and imports no framework, so any other renderer can use it too. Looking up a server module's stylesheets never compiles that module for the browser.
10
+
11
+ `@pitlane/assets/manifest` is the manifest the `assets()` Vite plugin from `@pitlane/assets/vite-plugin` supplies. Published as is, it is `{ mode: "unavailable" }`, and every lookup explains how to supply one. `@pitlane/assets/build` exports `createAssetManifest`, which turns another bundler's normalized output into the same manifest.
12
+
13
+ Generated manifests preserve source keys and environment names through JSON serialization, including names such as `__proto__`.
14
+
15
+ Chunk import maps are off by default. With `assets({ chunkImportMap: true })`, `renderImportMap({ value, nonce })` serializes the returned map into a `<script type="importmap">` string that delivers it safely. The Vite plugin requires Vite 8.1 or later.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Malstrom
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.
@@ -0,0 +1,23 @@
1
+ CSS development integration includes adaptations from @hiogawa/vite-plugin-fullstack.
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2024 Hiroshi Ogawa
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @pitlane/assets
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Framework-neutral asset resolution for server-rendered apps built with Vite or another bundler.
4
+
5
+ A server-rendered page has to name its browser entry script, its stylesheets, `modulepreload` hints, and the chunks of the islands it hydrates. Those URLs differ between `vite dev` and a production build. This package resolves source paths such as `app/entry.browser.ts` to the URLs the current dev server or build produced, through a resolver you construct from a manifest. The runtime imports no framework and no bundler. Its methods match the asset server from `remix/assets`, so it can be passed to Remix's `render({ assets })`. React, Preact, Vue, and plain HTML renderers use the returned strings directly.
6
+
7
+ ```sh
8
+ npm install @pitlane/assets
9
+ ```
10
+
11
+ Requires Node `^20.19.0 || >=22.12.0`. Install it as a runtime dependency, since application code imports the resolver. `vite` 8.1 or newer is an optional peer dependency, needed only for `@pitlane/assets/vite-plugin`.
12
+
13
+ | Entry point | Contents |
14
+ | ----------------------------- | ---------------------------------------------------------------- |
15
+ | `@pitlane/assets` | `createAssetResolver`, `renderImportMap`, and the manifest types |
16
+ | `@pitlane/assets/manifest` | The manifest a build integration supplies |
17
+ | `@pitlane/assets/build` | `createAssetManifest`, for integrations with other bundlers |
18
+ | `@pitlane/assets/vite-plugin` | `assets()`, the Vite integration |
19
+
20
+ ## Usage
21
+
22
+ Add the plugin to your Vite config:
23
+
24
+ ```ts
25
+ // vite.config.ts
26
+ import { assets } from "@pitlane/assets/vite-plugin";
27
+ import { defineConfig } from "vite";
28
+
29
+ export default defineConfig({
30
+ plugins: [assets()],
31
+ });
32
+ ```
33
+
34
+ Construct the resolver once, from the manifest the plugin supplies:
35
+
36
+ ```ts
37
+ // app/assets.ts
38
+ import { createAssetResolver } from "@pitlane/assets";
39
+ import manifest from "@pitlane/assets/manifest";
40
+
41
+ export let assets = createAssetResolver(manifest);
42
+
43
+ export let scriptEntry = await assets.getScriptEntry("app/entry.browser.ts");
44
+ export let stylesheets = await assets.getStylesheets("app/entry.server.ts");
45
+ ```
46
+
47
+ `scriptEntry` is `{ href, preloads, importMap }`: the entry's URL, its chunk and static JavaScript dependencies for `modulepreload` links, and the client import map. Under `vite dev`, `href` is the dev URL, `preloads` is empty, and the map is `{ imports: {} }`.
48
+
49
+ Keys are paths from the project root. `./app/x.ts`, `/app/x.ts`, `file:app/x.ts`, and `app/x.ts#Export` all name `app/x.ts`, and a module outside the root keeps its leading `../`. A string literal passed to a resolver's `getScriptEntry` or `getHref` in server code registers that module as a browser input: a script entry for `getScriptEntry`, an emitted asset for `getHref`. The receiver must be the result of `createAssetResolver()` imported from `@pitlane/assets` or `pitlane/assets`, a top-level variable holding one, or such a variable copied, imported, renamed, re-exported (including through `export *`), or default-exported from another module. Each such name must be declared only once in its module. A namespace import, a function parameter, `this`, another object's same-named method, a same-named function from another module, or a resolver imported from a dependency Vite externalizes registers nothing. List computed paths, and keys called through those receivers, in `assets({ include: [...] })`.
50
+
51
+ | Method | Returns |
52
+ | --- | --- |
53
+ | `getScriptEntry(key)` | `{ href, preloads, importMap }` for a browser entry |
54
+ | `getHref(key)` | The URL of a browser entry, stylesheet, or other asset |
55
+ | `getPreloads(key \| key[])` | `modulepreload` URLs for scripts, and the URL of an explicitly requested stylesheet |
56
+ | `getStylesheets(key \| key[])` | Stylesheets the modules need, from the client graph and the current server graph |
57
+ | `getImportMap(key \| key[])` | The complete client import map |
58
+
59
+ Looking up a module's stylesheets never compiles it for the browser, so `getStylesheets("app/entry.server.ts")` is safe for server code.
60
+
61
+ ## Chunk import maps
62
+
63
+ Off by default. `assets({ chunkImportMap: true })` enables Vite's chunk import maps for the client build, so a dependency's new hash changes one map entry instead of every importing chunk. Deliver the map before any `modulepreload` link or module script. `renderImportMap({ value, nonce })` returns it as a `<script type="importmap">` string, with every `<` escaped so the map cannot close the element, or `""` when the map is empty:
64
+
65
+ ```ts
66
+ import { renderImportMap } from "@pitlane/assets";
67
+
68
+ let html = renderImportMap({ value: scriptEntry.importMap });
69
+ ```
70
+
71
+ The option requires browser support for import maps and `import.meta.resolve`, and cannot be combined with Vite's `experimental.renderBuiltUrl`.
72
+
73
+ ## Without Vite
74
+
75
+ As published, `@pitlane/assets/manifest` exports `{ mode: "unavailable" }`. A resolver built from it constructs normally and rejects every lookup with instructions for supplying a manifest, so tests and type checkers can import your `app/assets.ts`.
76
+
77
+ Other bundlers produce a manifest with `createAssetManifest(build)` from `@pitlane/assets/build`. It takes a normalized description of the emitted chunks, their modules, static and dynamic imports, and stylesheets, and computes preloads, stylesheet lists, and public URLs:
78
+
79
+ ```ts
80
+ import { createAssetManifest } from "@pitlane/assets/build";
81
+ import { createAssetResolver } from "@pitlane/assets";
82
+
83
+ let manifest = createAssetManifest({
84
+ base: "/",
85
+ environments: {
86
+ client: {
87
+ role: "client",
88
+ chunks: {
89
+ main: {
90
+ file: "assets/main-a1.js",
91
+ modules: ["src/main.ts"],
92
+ imports: [],
93
+ dynamicImports: [],
94
+ stylesheets: ["assets/main-b2.css"],
95
+ },
96
+ },
97
+ entries: { "src/main.ts": "main" },
98
+ assets: {},
99
+ },
100
+ },
101
+ });
102
+
103
+ let assets = createAssetResolver(manifest);
104
+ await assets.getScriptEntry("src/main.ts");
105
+ // { href: "/assets/main-a1.js", preloads: ["/assets/main-a1.js"], importMap: { imports: {} } }
106
+ ```
107
+
108
+ ## Documentation
109
+
110
+ - [Asset resolution](https://pitlane.tools/guides/assets): resolver methods, source keys, registration, Remix and non-Remix documents, chunk import maps, and errors.
111
+ - [Manifest integrations](https://pitlane.tools/guides/asset-build): the `createAssetManifest` input model for integration authors.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,161 @@
1
+ import { n as quotedList, r as normalizeSourceKey } from "./specifier-DLC81sXY.mjs";
2
+ //#region src/generate.ts
3
+ const CHUNK_LISTS = [
4
+ "modules",
5
+ "imports",
6
+ "dynamicImports",
7
+ "stylesheets"
8
+ ];
9
+ /**
10
+ * Converts a bundler's normalized output into the manifest
11
+ * `createAssetResolver` reads.
12
+ *
13
+ * It walks each environment's static chunk graph to record, for every source
14
+ * module, its `modulepreload` URLs (its chunk first, then dependencies
15
+ * shallowest first) and stylesheets (a dependency's before its importer's).
16
+ * Dynamic imports are validated and never followed. It reads no files,
17
+ * invents no edges, and generates no import map; `serverEnvironment` is left
18
+ * for the integration to set per server.
19
+ *
20
+ * @throws When there is not exactly one client environment, an edge or entry
21
+ * names a missing chunk, a chunk lacks one of its lists, a server environment
22
+ * lists entries, a source key is an absolute file URL, or one source key maps
23
+ * to two URLs.
24
+ */
25
+ function createAssetManifest(build) {
26
+ let clients = Object.keys(build.environments).filter((name) => build.environments[name].role === "client");
27
+ if (clients.length !== 1) {
28
+ let found = clients.length === 0 ? "none" : quotedList(clients, "conjunction");
29
+ throw new Error(`createAssetManifest() needs exactly one client environment; found ${found}.`);
30
+ }
31
+ let publicUrl = (file) => resolvePublicUrl(build.base, file);
32
+ let environments = Object.create(null);
33
+ let entries = new SourceIndex();
34
+ let assets = new SourceIndex();
35
+ for (let [name, environment] of Object.entries(build.environments)) {
36
+ validateEnvironment(name, environment);
37
+ let entryChunks = /* @__PURE__ */ new Map();
38
+ for (let [rawKey, chunkId] of Object.entries(environment.entries)) {
39
+ let key = sourceKey(name, rawKey);
40
+ entries.record(key, publicUrl(environment.chunks[chunkId].file), name);
41
+ entryChunks.set(key, chunkId);
42
+ }
43
+ for (let [rawKey, file] of Object.entries(environment.assets)) assets.record(sourceKey(name, rawKey), publicUrl(file), name);
44
+ environments[name] = {
45
+ role: environment.role,
46
+ modules: indexModules(name, environment, entryChunks, publicUrl)
47
+ };
48
+ }
49
+ return {
50
+ mode: "build",
51
+ environments,
52
+ entries: entries.urls,
53
+ assets: assets.urls,
54
+ importMap: build.importMap ?? { imports: {} }
55
+ };
56
+ }
57
+ function validateEnvironment(name, environment) {
58
+ let { chunks } = environment;
59
+ let where = (chunkId) => `chunk "${chunkId}" in the "${name}" environment`;
60
+ for (let [chunkId, chunk] of Object.entries(chunks)) {
61
+ if (typeof chunk.file !== "string") throw new Error(`createAssetManifest(): ${where(chunkId)} has no file.`);
62
+ for (let list of CHUNK_LISTS) if (!Array.isArray(chunk[list])) throw new Error(`createAssetManifest(): ${where(chunkId)} has no ${list} list.`);
63
+ }
64
+ for (let [chunkId, chunk] of Object.entries(chunks)) {
65
+ let edges = [...chunk.imports.map((target) => ["imports", target]), ...chunk.dynamicImports.map((target) => ["dynamically imports", target])];
66
+ for (let [verb, target] of edges) if (!Object.hasOwn(chunks, target)) throw new Error(`createAssetManifest(): ${where(chunkId)} ${verb} "${target}", which is not a chunk in that environment.`);
67
+ }
68
+ let entryKeys = Object.keys(environment.entries);
69
+ if (environment.role === "server" && entryKeys.length > 0) throw new Error(`createAssetManifest(): the "${name}" environment lists browser entries, but only the client environment has them.`);
70
+ for (let key of entryKeys) {
71
+ let chunkId = environment.entries[key];
72
+ if (!Object.hasOwn(chunks, chunkId)) throw new Error(`createAssetManifest(): browser entry "${key}" in the "${name}" environment names chunk "${chunkId}", which is not a chunk in that environment.`);
73
+ }
74
+ }
75
+ /**
76
+ * Every source module's metadata: the union over each chunk it belongs to,
77
+ * with a browser entry's own chunk first so its preloads lead with its URL.
78
+ */
79
+ function indexModules(name, environment, entryChunks, publicUrl) {
80
+ let memberships = /* @__PURE__ */ new Map();
81
+ let join = (key, chunkId) => {
82
+ let chunkIds = memberships.get(key) ?? [];
83
+ if (!chunkIds.includes(chunkId)) chunkIds.push(chunkId);
84
+ memberships.set(key, chunkIds);
85
+ };
86
+ for (let [key, chunkId] of entryChunks) join(key, chunkId);
87
+ for (let [chunkId, chunk] of Object.entries(environment.chunks)) for (let rawKey of chunk.modules) join(sourceKey(name, rawKey), chunkId);
88
+ let closures = /* @__PURE__ */ new Map();
89
+ let closureOf = (chunkId) => {
90
+ let closure = closures.get(chunkId) ?? staticClosure(environment.chunks, chunkId, publicUrl);
91
+ closures.set(chunkId, closure);
92
+ return closure;
93
+ };
94
+ let modules = Object.create(null);
95
+ for (let [key, chunkIds] of memberships) {
96
+ let preloads = /* @__PURE__ */ new Set();
97
+ let stylesheets = /* @__PURE__ */ new Set();
98
+ for (let closure of chunkIds.map(closureOf)) {
99
+ for (let href of closure.preloads) preloads.add(href);
100
+ for (let href of closure.stylesheets) stylesheets.add(href);
101
+ }
102
+ modules[key] = {
103
+ preloads: [...preloads],
104
+ stylesheets: [...stylesheets]
105
+ };
106
+ }
107
+ return modules;
108
+ }
109
+ /**
110
+ * The chunks reachable from `root` through static imports. Preloads are in
111
+ * breadth-first order, so shallower chunks come first; stylesheets are in
112
+ * post-order, so a dependency's styles precede its importer's, matching the
113
+ * order the modules evaluate. Each chunk is visited once, which ends cycles.
114
+ */
115
+ function staticClosure(chunks, root, publicUrl) {
116
+ let order = [root];
117
+ let queued = new Set(order);
118
+ for (let index = 0; index < order.length; index++) for (let dependency of chunks[order[index]].imports) {
119
+ if (queued.has(dependency)) continue;
120
+ queued.add(dependency);
121
+ order.push(dependency);
122
+ }
123
+ let stylesheets = /* @__PURE__ */ new Set();
124
+ let visited = /* @__PURE__ */ new Set();
125
+ let visit = (chunkId) => {
126
+ if (visited.has(chunkId)) return;
127
+ visited.add(chunkId);
128
+ let chunk = chunks[chunkId];
129
+ for (let dependency of chunk.imports) visit(dependency);
130
+ for (let file of chunk.stylesheets) stylesheets.add(publicUrl(file));
131
+ };
132
+ visit(root);
133
+ return {
134
+ preloads: [...new Set(order.map((chunkId) => publicUrl(chunks[chunkId].file)))],
135
+ stylesheets: [...stylesheets]
136
+ };
137
+ }
138
+ function sourceKey(environment, rawKey) {
139
+ let key = normalizeSourceKey(rawKey);
140
+ if (key !== null) return key;
141
+ throw new Error(`createAssetManifest(): source key "${rawKey}" in the "${environment}" environment is an absolute file URL. Normalize it to a path relative to the project root.`);
142
+ }
143
+ /** `key → url` across environments, rejecting a second, different URL for one key. */
144
+ var SourceIndex = class {
145
+ urls = Object.create(null);
146
+ #environments = Object.create(null);
147
+ record(key, url, environment) {
148
+ let existing = Object.hasOwn(this.urls, key) ? this.urls[key] : void 0;
149
+ if (existing !== void 0 && existing !== url) throw new Error(`createAssetManifest(): source key "${key}" maps to "${existing}" in the "${this.#environments[key]}" environment and "${url}" in the "${environment}" environment.`);
150
+ this.urls[key] = url;
151
+ this.#environments[key] = environment;
152
+ }
153
+ };
154
+ /** Absolute URLs (`/x`, `//host/x`, `https://…`) stay as they are; relative files join the base. */
155
+ function resolvePublicUrl(base, file) {
156
+ if (/^(?:[a-z][a-z\d+.-]*:|\/)/i.test(file)) return file;
157
+ if (base === "" || base.endsWith("/")) return base + file;
158
+ return `${base}/${file}`;
159
+ }
160
+ //#endregion
161
+ export { createAssetManifest as t };
@@ -0,0 +1,21 @@
1
+ import { a as AssetMetadata, i as AssetEnvironment, l as ImportMap, n as AssetBuildChunk, r as AssetBuildEnvironment, s as BuildAssetsManifest, t as AssetBuild } from "./types-DlTsImzF.mjs";
2
+ //#region src/generate.d.ts
3
+ /**
4
+ * Converts a bundler's normalized output into the manifest
5
+ * `createAssetResolver` reads.
6
+ *
7
+ * It walks each environment's static chunk graph to record, for every source
8
+ * module, its `modulepreload` URLs (its chunk first, then dependencies
9
+ * shallowest first) and stylesheets (a dependency's before its importer's).
10
+ * Dynamic imports are validated and never followed. It reads no files,
11
+ * invents no edges, and generates no import map; `serverEnvironment` is left
12
+ * for the integration to set per server.
13
+ *
14
+ * @throws When there is not exactly one client environment, an edge or entry
15
+ * names a missing chunk, a chunk lacks one of its lists, a server environment
16
+ * lists entries, a source key is an absolute file URL, or one source key maps
17
+ * to two URLs.
18
+ */
19
+ export declare function createAssetManifest(build: AssetBuild): BuildAssetsManifest;
20
+ //#endregion
21
+ export type { AssetBuild, AssetBuildChunk, AssetBuildEnvironment, AssetEnvironment, AssetMetadata, BuildAssetsManifest, ImportMap };
package/dist/build.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as createAssetManifest } from "./build-BbA-Zq1m.mjs";
2
+ export { createAssetManifest };
@@ -0,0 +1,48 @@
1
+ import { a as AssetMetadata, c as DevAssetsManifest, i as AssetEnvironment, l as ImportMap, o as AssetsManifest, s as BuildAssetsManifest, u as ScriptEntry } from "./types-DlTsImzF.mjs";
2
+ //#region src/resolver.d.ts
3
+ /**
4
+ * Resolves portable source keys, such as `app/entry.browser.ts` or
5
+ * `file:app/counter.tsx#Counter`, to the URLs a build or development server
6
+ * produced for them.
7
+ *
8
+ * Structurally compatible with the resolution methods of a `remix/assets`
9
+ * asset server, so it can be passed to `render({ assets })`.
10
+ */
11
+ interface AssetResolver {
12
+ /** A registered browser entry's URL, `modulepreload` URLs, and the client import map. */
13
+ getScriptEntry(path: string): Promise<ScriptEntry>;
14
+ /** The URL of a registered browser entry, stylesheet, or other asset. */
15
+ getHref(path: string): Promise<string>;
16
+ /** Scripts' chunks and static JavaScript dependencies, and explicitly requested stylesheets. */
17
+ getPreloads(path: string | readonly string[]): Promise<string[]>;
18
+ /** Stylesheets the modules need, from the client graph and the current server graph. */
19
+ getStylesheets(path: string | readonly string[]): Promise<string[]>;
20
+ /** The complete client import map, after checking the keys belong to the client graph. */
21
+ getImportMap(path: string | readonly string[]): Promise<ImportMap>;
22
+ }
23
+ /**
24
+ * Constructs the resolver for a manifest, usually the default export of
25
+ * `@pitlane/assets/manifest`. Construct it once, in a module of the app's own.
26
+ */
27
+ export declare function createAssetResolver(manifest: AssetsManifest): AssetResolver;
28
+ //#endregion
29
+ //#region src/import-map.d.ts
30
+ /** Options for {@link renderImportMap}. */
31
+ interface RenderImportMapOptions {
32
+ /** The map to serialize, such as `ScriptEntry.importMap`. */
33
+ value: ImportMap;
34
+ /** A nonce from the application's existing Content Security Policy. */
35
+ nonce?: string;
36
+ }
37
+ /**
38
+ * Serializes an import map into an inline `<script type="importmap">` element
39
+ * for server-rendered HTML, or `""` when the map has no mappings.
40
+ *
41
+ * Every `<` in the JSON is written as `\u003c`, so no key or URL can end the
42
+ * script element; the browser parses the same strings back. The nonce is
43
+ * written as an escaped, quoted attribute. Place the result before any
44
+ * `modulepreload` link or module script.
45
+ */
46
+ export declare function renderImportMap({ value, nonce }: RenderImportMapOptions): string;
47
+ //#endregion
48
+ export type { AssetEnvironment, AssetMetadata, AssetResolver, AssetsManifest, BuildAssetsManifest, DevAssetsManifest, ImportMap, RenderImportMapOptions, ScriptEntry };
package/dist/index.mjs ADDED
@@ -0,0 +1,146 @@
1
+ import { n as quotedList, r as normalizeSourceKey, t as assetsSpecifier } from "./specifier-DLC81sXY.mjs";
2
+ //#region src/resolver.ts
3
+ /**
4
+ * Constructs the resolver for a manifest, usually the default export of
5
+ * `@pitlane/assets/manifest`. Construct it once, in a module of the app's own.
6
+ */
7
+ function createAssetResolver(manifest) {
8
+ if (manifest.mode === "unavailable") return unavailableResolver();
9
+ return manifestResolver(manifest);
10
+ }
11
+ function unavailableResolver() {
12
+ let fail = (method) => async (path) => {
13
+ throw new Error(`${call(method, path)} has no asset manifest to read: ${assetsSpecifier("manifest")} was not supplied by a build integration. Add assets() from ${assetsSpecifier("vite-plugin")} to your Vite config, or pass createAssetResolver() a manifest from createAssetManifest() in ${assetsSpecifier("build")}.`);
14
+ };
15
+ return {
16
+ getScriptEntry: fail("getScriptEntry"),
17
+ getHref: fail("getHref"),
18
+ getPreloads: fail("getPreloads"),
19
+ getStylesheets: fail("getStylesheets"),
20
+ getImportMap: fail("getImportMap")
21
+ };
22
+ }
23
+ function manifestResolver(manifest) {
24
+ let { environments, entries, assets } = manifest;
25
+ let clientName = Object.keys(environments).find((name) => environments[name].role === "client") ?? "client";
26
+ let stylesheetEnvironments = [.../* @__PURE__ */ new Set([clientName, manifest.serverEnvironment ?? clientName])];
27
+ /** The client graph's metadata for a key, which a registered entry always has. */
28
+ function clientMetadata(key) {
29
+ let metadata = own(environments[clientName]?.modules ?? {}, key);
30
+ if (metadata) return metadata;
31
+ let href = own(entries, key);
32
+ return href === void 0 ? void 0 : {
33
+ preloads: [href],
34
+ stylesheets: []
35
+ };
36
+ }
37
+ function requireClientMetadata(method, path, key) {
38
+ let metadata = clientMetadata(key);
39
+ if (metadata) return metadata;
40
+ throw new Error(`${call(method, path)} found no module "${key}" in the "${clientName}" environment's graph.`);
41
+ }
42
+ function missingRegistration(method, path, key, what) {
43
+ let observedIn = Object.keys(environments).find((name) => Object.hasOwn(environments[name].modules, key));
44
+ let observation = observedIn ? `The module is in the "${observedIn}" graph but is not registered as a browser entry.` : "No environment's graph contains the module.";
45
+ return /* @__PURE__ */ new Error(`${call(method, path)} found no ${what} "${key}" in the "${clientName}" environment. ${observation} Pass the key to ${method}() as a string literal in server code, or list it in assets({ include }) from ${assetsSpecifier("vite-plugin")}.`);
46
+ }
47
+ return {
48
+ async getScriptEntry(path) {
49
+ let key = sourceKey("getScriptEntry", path);
50
+ let href = own(entries, key);
51
+ if (href === void 0) throw missingRegistration("getScriptEntry", path, key, "browser entry");
52
+ if (manifest.mode === "dev") return {
53
+ href,
54
+ preloads: [],
55
+ importMap: { imports: {} }
56
+ };
57
+ return {
58
+ href,
59
+ preloads: [...clientMetadata(key).preloads],
60
+ importMap: copyImportMap(manifest.importMap)
61
+ };
62
+ },
63
+ async getHref(path) {
64
+ let key = sourceKey("getHref", path);
65
+ let href = own(entries, key) ?? own(assets, key);
66
+ if (href === void 0) throw missingRegistration("getHref", path, key, "browser entry or asset");
67
+ return href;
68
+ },
69
+ async getPreloads(paths) {
70
+ let preloads = /* @__PURE__ */ new Set();
71
+ for (let path of asList(paths)) {
72
+ let key = sourceKey("getPreloads", path);
73
+ let stylesheet = own(assets, key);
74
+ if (stylesheet !== void 0 && /\.css(?:[?#]|$)/.test(stylesheet)) {
75
+ preloads.add(stylesheet);
76
+ continue;
77
+ }
78
+ for (let href of requireClientMetadata("getPreloads", path, key).preloads) preloads.add(href);
79
+ }
80
+ return manifest.mode === "dev" ? [] : [...preloads];
81
+ },
82
+ async getStylesheets(paths) {
83
+ let stylesheets = /* @__PURE__ */ new Set();
84
+ for (let path of asList(paths)) {
85
+ let key = sourceKey("getStylesheets", path);
86
+ let found = stylesheetEnvironments.flatMap((name) => {
87
+ let graph = own(environments, name);
88
+ return (graph && own(graph.modules, key)) ?? [];
89
+ });
90
+ if (found.length === 0) throw new Error(`${call("getStylesheets", path)} found no module "${key}" in the ${quotedList(stylesheetEnvironments, "disjunction")} environment's graph.`);
91
+ for (let href of found.flatMap((metadata) => metadata.stylesheets)) stylesheets.add(href);
92
+ }
93
+ return [...stylesheets];
94
+ },
95
+ async getImportMap(paths) {
96
+ let list = asList(paths);
97
+ for (let path of list) requireClientMetadata("getImportMap", path, sourceKey("getImportMap", path));
98
+ if (list.length === 0 || manifest.mode === "dev") return { imports: {} };
99
+ return copyImportMap(manifest.importMap);
100
+ }
101
+ };
102
+ }
103
+ /** Reads a source key's record without reaching `Object.prototype` members such as `constructor`. */
104
+ function own(record, key) {
105
+ return Object.hasOwn(record, key) ? record[key] : void 0;
106
+ }
107
+ /** Callers merge their own mappings into the returned map; the manifest's stays intact. */
108
+ function copyImportMap(map) {
109
+ return structuredClone(map);
110
+ }
111
+ function sourceKey(method, path) {
112
+ let key = normalizeSourceKey(path);
113
+ if (key !== null) return key;
114
+ throw new Error(`${call(method, path)} received an absolute file URL. Pass the source key relative to the project root instead, such as "file:app/counter.tsx" or "app/counter.tsx".`);
115
+ }
116
+ function asList(paths) {
117
+ return typeof paths === "string" ? [paths] : paths;
118
+ }
119
+ /** How a message names the call that failed, such as `assets.getHref("app/logo.svg")`. */
120
+ function call(method, path) {
121
+ return `assets.${method}(${JSON.stringify(path)})`;
122
+ }
123
+ //#endregion
124
+ //#region src/import-map.ts
125
+ /**
126
+ * Serializes an import map into an inline `<script type="importmap">` element
127
+ * for server-rendered HTML, or `""` when the map has no mappings.
128
+ *
129
+ * Every `<` in the JSON is written as `\u003c`, so no key or URL can end the
130
+ * script element; the browser parses the same strings back. The nonce is
131
+ * written as an escaped, quoted attribute. Place the result before any
132
+ * `modulepreload` link or module script.
133
+ */
134
+ function renderImportMap({ value, nonce }) {
135
+ if (!hasMappings(value)) return "";
136
+ let json = JSON.stringify(value).replaceAll("<", "\\u003c");
137
+ return `<script type="importmap"${nonce === void 0 ? "" : ` nonce="${escapeAttribute(nonce)}"`}>${json}<\/script>`;
138
+ }
139
+ function hasMappings({ imports, scopes = {}, integrity = {} }) {
140
+ return Object.keys(imports).length > 0 || Object.values(scopes).some((scope) => Object.keys(scope).length > 0) || Object.keys(integrity).length > 0;
141
+ }
142
+ function escapeAttribute(text) {
143
+ return text.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
144
+ }
145
+ //#endregion
146
+ export { createAssetResolver, renderImportMap };
@@ -0,0 +1,17 @@
1
+ import { o as AssetsManifest } from "./types-DlTsImzF.mjs";
2
+ //#region src/manifest.d.ts
3
+ /**
4
+ * The manifest `@pitlane/assets/manifest` exports when no build integration
5
+ * replaced the module. A resolver constructed from it works, and every lookup
6
+ * explains how to supply a real manifest.
7
+ *
8
+ * `assets()` from `@pitlane/assets/vite-plugin` replaces this module with the
9
+ * manifest of the current dev server or build. A graph Vite did not build can
10
+ * alias the module to its own, or pass a manifest to `createAssetResolver`
11
+ * directly.
12
+ *
13
+ * @module @pitlane/assets/manifest
14
+ */
15
+ declare let manifest: AssetsManifest;
16
+ //#endregion
17
+ export { manifest as default };
@@ -0,0 +1,16 @@
1
+ //#region src/manifest.ts
2
+ /**
3
+ * The manifest `@pitlane/assets/manifest` exports when no build integration
4
+ * replaced the module. A resolver constructed from it works, and every lookup
5
+ * explains how to supply a real manifest.
6
+ *
7
+ * `assets()` from `@pitlane/assets/vite-plugin` replaces this module with the
8
+ * manifest of the current dev server or build. A graph Vite did not build can
9
+ * alias the module to its own, or pass a manifest to `createAssetResolver`
10
+ * directly.
11
+ *
12
+ * @module @pitlane/assets/manifest
13
+ */
14
+ let manifest = { mode: "unavailable" };
15
+ //#endregion
16
+ export { manifest as default };
@@ -0,0 +1,47 @@
1
+ //#region src/source-key.ts
2
+ /**
3
+ * Reduces any accepted spelling of a source module to its portable key: the
4
+ * slash-separated path from the project root, such as `app/counter.tsx` or,
5
+ * for a linked module outside the root, `../shared/widget.ts`.
6
+ *
7
+ * `./`, a leading `/` (a project-root marker), `file:`, and an `#export`
8
+ * fragment are dropped, and dot segments collapse lexically. Returns `null`
9
+ * for an absolute file URL, whose meaning depends on the machine that wrote
10
+ * it.
11
+ */
12
+ function normalizeSourceKey(path) {
13
+ let key = path;
14
+ let fragment = key.indexOf("#");
15
+ if (fragment !== -1) key = key.slice(0, fragment);
16
+ if (key.startsWith("file:")) {
17
+ key = key.slice(5);
18
+ if (key.startsWith("/")) return null;
19
+ }
20
+ let segments = [];
21
+ for (let segment of key.split("/")) {
22
+ if (segment === "" || segment === ".") continue;
23
+ if (segment === ".." && segments.length > 0 && segments.at(-1) !== "..") segments.pop();
24
+ else segments.push(segment);
25
+ }
26
+ return segments.join("/");
27
+ }
28
+ //#endregion
29
+ //#region src/specifier.ts
30
+ /**
31
+ * Each module of the `pitlane` umbrella adds the package it re-exports to
32
+ * this set when it loads, so a package can name itself the way the app
33
+ * imports it. An app that installed only `pitlane` cannot import
34
+ * `@pitlane/assets/vite-plugin` under a strict package manager.
35
+ */
36
+ const UMBRELLA_PACKAGES = Symbol.for("pitlane.umbrella.packages");
37
+ /** The specifier the app imports `subpath` of this package by, for error messages. */
38
+ function assetsSpecifier(subpath) {
39
+ return `${globalThis[UMBRELLA_PACKAGES]?.has("@pitlane/assets") ? "pitlane/assets" : "@pitlane/assets"}/${subpath}`;
40
+ }
41
+ /** Quotes and joins names for a message: `"a"`, `"a" and "b"`, `"a", "b", and "c"`. */
42
+ function quotedList(names, type) {
43
+ let quoted = names.map((name) => `"${name}"`);
44
+ return new Intl.ListFormat("en", { type }).format(quoted);
45
+ }
46
+ //#endregion
47
+ export { quotedList as n, normalizeSourceKey as r, assetsSpecifier as t };