@valfuse-node/assets 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 valfuse-node contributors
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 CHANGED
@@ -1,3 +1,247 @@
1
- # Temporary Holding Version
1
+ # @valfuse-node/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
+ Generate a typed TypeScript registry for static assets. Directory names and
4
+ file names become nested keys, and each leaf contains the asset's public URL.
5
+
6
+ ```bash
7
+ npm install --save-dev @valfuse-node/assets
8
+ ```
9
+
10
+ Requires **Node.js 22 or newer** to run the generator. The generated module is
11
+ plain TypeScript and has no runtime dependency on this package.
12
+
13
+ The package declares `engines.node >=22`. Install it as a development
14
+ dependency when you generate the registry during development or CI.
15
+
16
+ ## Contents
17
+
18
+ - [Quick start](#quick-start)
19
+ - [Configuration](#configuration)
20
+ - [Generated names and paths](#generated-names-and-paths)
21
+ - [Programmatic API](#programmatic-api)
22
+ - [Troubleshooting](#troubleshooting)
23
+ - [Scope](#scope)
24
+ - [Release history](#release-history)
25
+ - [Support](#support)
26
+ - [License](#license)
27
+
28
+ ## Quick start
29
+
30
+ Place files under `public/assets`:
31
+
32
+ ```text
33
+ public/assets/
34
+ ├── images/
35
+ │ ├── company-logo.svg
36
+ │ └── profile/avatar.png
37
+ └── icons/search.svg
38
+ ```
39
+
40
+ Create `valfuse-assets.yaml` in the project root:
41
+
42
+ ```yaml
43
+ input_dir: public/assets
44
+ output_file: src/assets/assets.ts
45
+ base_path: /assets
46
+ ```
47
+
48
+ Run `npx valfuse-assets`. It generates `src/assets/assets.ts`:
49
+
50
+ ```ts
51
+ import { assets } from "./assets/assets";
52
+
53
+ assets.images.companyLogo; // "/assets/images/company-logo.svg"
54
+ assets.images.profile.avatar; // "/assets/images/profile/avatar.png"
55
+ assets.icons.search; // "/assets/icons/search.svg"
56
+ ```
57
+
58
+ The generator also exports `AssetPath`, a union of all generated URL strings:
59
+
60
+ ```ts
61
+ import { assets, type AssetPath } from "./assets/assets";
62
+
63
+ const logo: AssetPath = assets.images.companyLogo;
64
+ ```
65
+
66
+ The path values work with React, Vue, HTML, and other browser code that accepts
67
+ URLs. Put assets in the framework's public/static directory so the framework
68
+ copies them to the matching URL at build time.
69
+
70
+ For example, in a React component:
71
+
72
+ ```tsx
73
+ import { assets } from "./assets/assets";
74
+
75
+ export function CompanyLogo() {
76
+ return <img src={assets.images.companyLogo} alt="Company" />;
77
+ }
78
+ ```
79
+
80
+ The generated registry contains URL strings only. Keep the source files in the
81
+ public directory and make sure the configured `base_path` matches the URL path
82
+ your server or CDN uses to serve that directory.
83
+
84
+ ## Configuration
85
+
86
+ The CLI reads `valfuse-assets.yaml` from the current working directory, matching
87
+ the config-file convention used by the localization CLI:
88
+
89
+ ```yaml
90
+ input_dir: public/media
91
+ output_file: src/assets/assets.ts
92
+ base_path: /media
93
+ ```
94
+
95
+ Relative file paths in this config are resolved from the current working
96
+ directory; absolute file paths are also accepted. The `base_path` is the URL
97
+ prefix that serves the contents of `input_dir`; it can be a root-relative path
98
+ such as `/media` or an absolute HTTP(S) URL for a CDN.
99
+ Each discovered file's path beneath `input_dir` is appended to this prefix.
100
+ Input and output paths are resolved from the current working directory. The
101
+ CLI accepts only these snake_case YAML keys; unknown keys, empty values, an
102
+ invalid URL prefix, or an output filename without a `.ts` extension are errors.
103
+
104
+ The default values are:
105
+
106
+ | Option | Default | Purpose |
107
+ | ------------- | ---------------------- | ------------------------------------------ |
108
+ | `input_dir` | `public/assets` | Directory tree to scan |
109
+ | `output_file` | `src/assets/assets.ts` | Generated TypeScript module |
110
+ | `base_path` | `/assets` | Public URL prefix for files in `input_dir` |
111
+
112
+ Pass another config file with `--config`:
113
+
114
+ ```bash
115
+ npx valfuse-assets --config config/valfuse-assets.yaml
116
+ ```
117
+
118
+ Use `--check` in CI to fail when the generated module is missing or stale. It
119
+ does not write files:
120
+
121
+ ```bash
122
+ npx valfuse-assets --check
123
+ npx valfuse-assets --help
124
+ ```
125
+
126
+ | Option | Behavior |
127
+ | ----------------- | --------------------------------------------- |
128
+ | `--config <file>` | Read another YAML config file |
129
+ | `--check` | Fail if the generated module is missing/stale |
130
+ | `--help`, `-h` | Print CLI usage and defaults |
131
+
132
+ For repeatable project commands, add scripts to your `package.json`:
133
+
134
+ ```json
135
+ {
136
+ "scripts": {
137
+ "assets:generate": "valfuse-assets",
138
+ "assets:check": "valfuse-assets --check"
139
+ }
140
+ }
141
+ ```
142
+
143
+ Run `npm run assets:generate` after changing files under the input directory.
144
+ Run `npm run assets:check` in CI to ensure the committed generated registry is
145
+ current. The command does not copy, transform, or fingerprint source files.
146
+
147
+ ## Generated names and paths
148
+
149
+ - Nested folders remain nested object keys.
150
+ - File extensions are removed from keys: `company-logo.svg` becomes
151
+ `companyLogo`.
152
+ - Folder and file names are converted to lower camel case, including names that
153
+ already use camel case. A leading number is prefixed with `_`.
154
+ - URL segments are percent-encoded while the generated keys use readable names.
155
+ - Output is sorted deterministically, and unchanged output files are not
156
+ rewritten.
157
+ - Name collisions after normalization produce an error instead of silently
158
+ replacing an asset.
159
+ - Symlinks, special filesystem entries, `.DS_Store`, `Thumbs.db`, and
160
+ `desktop.ini` are skipped.
161
+
162
+ ## Programmatic API
163
+
164
+ The Node.js API is also available from the explicit
165
+ **@valfuse-node/core/assets** subpath. It is not loaded from the core root
166
+ import; use the standalone **@valfuse-node/assets** package when you only need
167
+ the CLI.
168
+
169
+ The generator can also be called from a Node.js build script:
170
+
171
+ ```ts
172
+ import { generateAssets } from "@valfuse-node/assets";
173
+
174
+ await generateAssets({
175
+ config: {
176
+ inputDir: "public/assets",
177
+ outputFile: "src/assets/assets.ts",
178
+ basePath: "/assets",
179
+ },
180
+ });
181
+ ```
182
+
183
+ `GenerateAssetsOptions` accepts an optional `cwd`, partial camelCase `config`,
184
+ and `check` flag. The equivalent of the default config is:
185
+
186
+ ```ts
187
+ {
188
+ inputDir: "public/assets",
189
+ outputFile: "src/assets/assets.ts",
190
+ basePath: "/assets",
191
+ }
192
+ ```
193
+
194
+ The package entry exports `generateAssets` and
195
+ `DEFAULT_ASSET_GENERATOR_CONFIG`. Its public types are
196
+ `AssetGeneratorConfig`, `AssetGeneratorConfigInput`, `GenerateAssetsOptions`,
197
+ and `GenerateAssetsResult`.
198
+
199
+ Pass `check: true` to check generated output without writing it. On success,
200
+ `generateAssets` returns `GenerateAssetsResult`:
201
+
202
+ ```ts
203
+ {
204
+ assetCount: number;
205
+ outputFile: string; // absolute path
206
+ changed: boolean;
207
+ }
208
+ ```
209
+
210
+ The function rejects missing or invalid input directories, invalid config, and
211
+ asset-name collisions with an `Error`. In check mode, it also throws when the
212
+ generated file is missing or stale.
213
+
214
+ ## Troubleshooting
215
+
216
+ - **Input directory not found:** run the CLI from the project root or set
217
+ `cwd` in the API call; configured paths are relative to that directory.
218
+ - **Two files map to the same TypeScript key:** rename one path segment. For
219
+ example, names that normalize to the same lower-camel-case key cannot share
220
+ the registry.
221
+ - **A generated URL returns 404:** compare `base_path` with the actual public
222
+ URL for the configured input directory. The generator does not move files.
223
+ - **CI reports stale output:** run `npm run assets:generate` and commit the
224
+ updated generated module.
225
+
226
+ ## Scope
227
+
228
+ This package generates a typed registry of static public URLs. It does not
229
+ import files into Vite/Webpack bundles, transform image files, or generate
230
+ image, SVG, font, or color helper APIs. For bundled assets, use your bundler's
231
+ asset import or `new URL(..., import.meta.url)` behavior instead.
232
+
233
+ ## Release history
234
+
235
+ See the [monorepo changelog](https://github.com/alpinnz/valfuse-node/blob/master/CHANGELOG.md).
236
+
237
+ ## Support
238
+
239
+ For usage questions, bug reports, and feature requests, see the
240
+ [project support guide](https://github.com/alpinnz/valfuse-node/blob/master/SUPPORT.md).
241
+ For security reports, follow the
242
+ [security policy](https://github.com/alpinnz/valfuse-node/blob/master/SECURITY.md).
243
+ Do not use public issues for security reports.
244
+
245
+ ## License
246
+
247
+ [MIT](LICENSE)
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { readFile as readFile2 } from "fs/promises";
5
+ import { relative as relative2, resolve as resolve2 } from "path";
6
+ import YAML from "yaml";
7
+
8
+ // src/generator.ts
9
+ import { lstat, mkdir, readFile, readdir, writeFile } from "fs/promises";
10
+ import { dirname, extname, relative, resolve } from "path";
11
+ var DEFAULT_ASSET_GENERATOR_CONFIG = Object.freeze({
12
+ inputDir: "public/assets",
13
+ outputFile: "src/assets/assets.ts",
14
+ basePath: "/assets"
15
+ });
16
+ var IGNORED_SYSTEM_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
17
+ async function generateAssets(options = {}) {
18
+ const cwd = resolve(options.cwd ?? process.cwd());
19
+ const config = normalizeConfig(options.config);
20
+ const inputDir = resolve(cwd, config.inputDir);
21
+ const outputFile = resolve(cwd, config.outputFile);
22
+ let inputStats;
23
+ try {
24
+ inputStats = await lstat(inputDir);
25
+ } catch (error) {
26
+ if (isNodeError(error) && error.code === "ENOENT") {
27
+ throw new Error(`Asset input directory not found: ${inputDir}`, { cause: error });
28
+ }
29
+ throw error;
30
+ }
31
+ if (!inputStats.isDirectory()) {
32
+ throw new Error(`Asset input must be a directory and cannot be a symlink: ${inputDir}`);
33
+ }
34
+ const files = await collectAssetFiles(inputDir, outputFile);
35
+ const tree = createAssetTree(files, config.basePath);
36
+ const source = renderAssetModule(tree);
37
+ const existingSource = await readExistingFile(outputFile);
38
+ const changed = existingSource !== source;
39
+ if (options.check) {
40
+ if (changed) {
41
+ throw new Error(
42
+ `Generated asset registry is missing or out of date: ${relative(cwd, outputFile)}. Run "npx valfuse-assets" to regenerate it.`
43
+ );
44
+ }
45
+ return { assetCount: files.length, outputFile, changed: false };
46
+ }
47
+ if (changed) {
48
+ await mkdir(dirname(outputFile), { recursive: true });
49
+ await writeFile(outputFile, source, "utf8");
50
+ }
51
+ return { assetCount: files.length, outputFile, changed };
52
+ }
53
+ function normalizeConfig(input = {}) {
54
+ const config = { ...DEFAULT_ASSET_GENERATOR_CONFIG, ...input };
55
+ for (const [key, value] of Object.entries(config)) {
56
+ if (typeof value !== "string" || value.trim() === "") {
57
+ throw new Error(`Config "${key}" must be a non-empty string.`);
58
+ }
59
+ }
60
+ if (/[?#\\]/.test(config.basePath)) {
61
+ throw new Error('Config "basePath" cannot contain a query, fragment, or backslash.');
62
+ }
63
+ if (!config.basePath.startsWith("/") && !/^https?:\/\//i.test(config.basePath)) {
64
+ throw new Error('Config "basePath" must start with "/" or be an absolute HTTP(S) URL.');
65
+ }
66
+ if (/^https?:\/\//i.test(config.basePath)) {
67
+ const url = new URL(config.basePath);
68
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
69
+ throw new Error('Config "basePath" must use HTTP or HTTPS.');
70
+ }
71
+ }
72
+ if (extname(config.outputFile).toLowerCase() !== ".ts") {
73
+ throw new Error('Config "outputFile" must have a .ts extension.');
74
+ }
75
+ return config;
76
+ }
77
+ async function collectAssetFiles(inputDir, outputFile) {
78
+ const files = [];
79
+ async function visit(directory, relativeDirectory) {
80
+ const entries = await readdir(directory, { withFileTypes: true });
81
+ entries.sort((left, right) => compareNames(left.name, right.name));
82
+ for (const entry of entries) {
83
+ if (IGNORED_SYSTEM_FILES.has(entry.name)) continue;
84
+ const absolutePath = resolve(directory, entry.name);
85
+ const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
86
+ if (entry.isDirectory()) {
87
+ await visit(absolutePath, relativePath);
88
+ continue;
89
+ }
90
+ if (!entry.isFile() || pathsEqual(absolutePath, outputFile)) continue;
91
+ files.push({ relativePath });
92
+ }
93
+ }
94
+ await visit(inputDir, "");
95
+ return files;
96
+ }
97
+ function createAssetTree(files, basePath) {
98
+ const root = { originalName: "", children: /* @__PURE__ */ new Map() };
99
+ const normalizedBasePath = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
100
+ for (const file of files) {
101
+ const segments = file.relativePath.split("/");
102
+ const url = buildAssetUrl(normalizedBasePath, segments);
103
+ let parent = root;
104
+ for (let index = 0; index < segments.length; index += 1) {
105
+ const originalName = segments[index];
106
+ const isFile = index === segments.length - 1;
107
+ const key = toAssetKey(originalName, isFile);
108
+ let child = parent.children.get(key);
109
+ if (child && child.originalName !== originalName) {
110
+ throw new Error(
111
+ `Asset names "${child.originalName}" and "${originalName}" both map to "${key}". Rename one path segment so generated keys stay unique.`
112
+ );
113
+ }
114
+ if (!child) {
115
+ child = { originalName, children: /* @__PURE__ */ new Map() };
116
+ parent.children.set(key, child);
117
+ }
118
+ if (isFile) {
119
+ if (child.url !== void 0 || child.children.size > 0) {
120
+ throw new Error(
121
+ `Asset path "${file.relativePath}" collides with another generated asset key.`
122
+ );
123
+ }
124
+ child.url = url;
125
+ } else if (child.url !== void 0) {
126
+ throw new Error(
127
+ `Asset directory "${file.relativePath}" collides with another generated asset key.`
128
+ );
129
+ }
130
+ parent = child;
131
+ }
132
+ }
133
+ return root;
134
+ }
135
+ function toAssetKey(pathSegment, isFile) {
136
+ const name = isFile ? pathSegment.slice(0, pathSegment.length - extname(pathSegment).length) : pathSegment;
137
+ const normalized = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
138
+ const words = normalized.match(/[A-Za-z0-9]+/g);
139
+ if (!words || words.length === 0) {
140
+ throw new Error(`Asset path segment "${pathSegment}" cannot be converted to a TypeScript key.`);
141
+ }
142
+ const [first, ...rest] = words;
143
+ const key = first.toLowerCase() + rest.map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
144
+ return /^[0-9]/.test(key) ? `_${key}` : key;
145
+ }
146
+ function buildAssetUrl(basePath, segments) {
147
+ const encodedPath = segments.map((segment) => encodeURIComponent(segment)).join("/");
148
+ return basePath ? `${basePath}/${encodedPath}` : `/${encodedPath}`;
149
+ }
150
+ function renderAssetModule(root) {
151
+ const lines = [
152
+ "/**",
153
+ " * Generated by @valfuse-node/assets. Do not edit manually.",
154
+ " */",
155
+ `export const assets = ${renderObject(root, 0)} as const;`,
156
+ "",
157
+ "type AssetLeaves<T> = T extends string",
158
+ " ? T",
159
+ " : T extends Readonly<Record<string, unknown>>",
160
+ " ? AssetLeaves<T[keyof T]>",
161
+ " : never;",
162
+ "",
163
+ "export type AssetPath = AssetLeaves<typeof assets>;",
164
+ "",
165
+ "export default assets;",
166
+ ""
167
+ ];
168
+ return lines.join("\n");
169
+ }
170
+ function renderObject(node, depth) {
171
+ const children = [...node.children.entries()].sort(
172
+ ([left], [right]) => compareNames(left, right)
173
+ );
174
+ if (children.length === 0) return "{}";
175
+ const indent = " ".repeat(depth);
176
+ const childIndent = " ".repeat(depth + 1);
177
+ const properties = children.map(([key, child]) => {
178
+ const value = child.url === void 0 ? renderObject(child, depth + 1) : JSON.stringify(child.url);
179
+ return `${childIndent}[${JSON.stringify(key)}]: ${value},`;
180
+ });
181
+ return `({
182
+ ${properties.join("\n")}
183
+ ${indent}})`;
184
+ }
185
+ async function readExistingFile(filePath) {
186
+ try {
187
+ return await readFile(filePath, "utf8");
188
+ } catch (error) {
189
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
190
+ throw error;
191
+ }
192
+ }
193
+ function pathsEqual(left, right) {
194
+ const resolvedLeft = resolve(left);
195
+ const resolvedRight = resolve(right);
196
+ return process.platform === "win32" ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
197
+ }
198
+ function compareNames(left, right) {
199
+ return left < right ? -1 : left > right ? 1 : 0;
200
+ }
201
+ function isNodeError(error) {
202
+ return error instanceof Error && "code" in error;
203
+ }
204
+
205
+ // src/cli/index.ts
206
+ var HELP = `valfuse-assets - generate typed TypeScript asset paths
207
+
208
+ Usage:
209
+ valfuse-assets [--config <file>] [--check]
210
+
211
+ Options:
212
+ --config <file> Read generator options from a YAML file
213
+ --check Exit with an error when the generated file is stale
214
+ --help, -h Show this help
215
+
216
+ The default config file is valfuse-assets.yaml. Defaults for omitted options:
217
+ input_dir: public/assets
218
+ output_file: src/assets/assets.ts
219
+ base_path: /assets
220
+ `;
221
+ async function main() {
222
+ const options = parseArguments(process.argv.slice(2));
223
+ if (options.help) {
224
+ console.log(HELP);
225
+ return;
226
+ }
227
+ const cwd = process.cwd();
228
+ const config = await readConfig(cwd, options.configFile);
229
+ const result = await generateAssets({ cwd, config, check: options.check });
230
+ console.log(
231
+ `${options.check ? "Asset registry is up to date" : result.changed ? "Generated asset registry" : "Asset registry is up to date"}: ${relative2(cwd, result.outputFile)} (${result.assetCount} assets)`
232
+ );
233
+ }
234
+ function parseArguments(args) {
235
+ const options = { check: false, help: false };
236
+ for (let index = 0; index < args.length; index += 1) {
237
+ const argument = args[index];
238
+ if (argument === "--check") {
239
+ options.check = true;
240
+ continue;
241
+ }
242
+ if (argument === "--help" || argument === "-h") {
243
+ options.help = true;
244
+ continue;
245
+ }
246
+ if (argument === "--config") {
247
+ const configFile = args[index + 1];
248
+ if (!configFile || configFile.startsWith("--")) {
249
+ throw new Error('Option "--config" requires a file path.');
250
+ }
251
+ options.configFile = configFile;
252
+ index += 1;
253
+ continue;
254
+ }
255
+ if (argument.startsWith("--config=")) {
256
+ const configFile = argument.slice("--config=".length);
257
+ if (!configFile) throw new Error('Option "--config" requires a file path.');
258
+ options.configFile = configFile;
259
+ continue;
260
+ }
261
+ throw new Error(`Unknown option "${argument}". Use --help to see available options.`);
262
+ }
263
+ return options;
264
+ }
265
+ async function readConfig(cwd, configFile) {
266
+ const filePath = resolve2(cwd, configFile ?? "valfuse-assets.yaml");
267
+ let content;
268
+ try {
269
+ content = await readFile2(filePath, "utf8");
270
+ } catch (error) {
271
+ if (isNodeError2(error) && error.code === "ENOENT") {
272
+ throw new Error(
273
+ `Asset config file not found: ${relative2(cwd, filePath)}. Create it or pass --config <file>.`,
274
+ { cause: error }
275
+ );
276
+ }
277
+ throw error;
278
+ }
279
+ let parsed;
280
+ try {
281
+ parsed = YAML.parse(content);
282
+ } catch (error) {
283
+ const detail = error instanceof Error ? `: ${error.message}` : "";
284
+ throw new Error(`Could not parse YAML asset config ${relative2(cwd, filePath)}${detail}.`, {
285
+ cause: error
286
+ });
287
+ }
288
+ return parseConfigObject(parsed, filePath);
289
+ }
290
+ function parseConfigObject(value, filePath) {
291
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
292
+ throw new Error(`Asset config must be a YAML mapping: ${filePath}`);
293
+ }
294
+ const config = {};
295
+ for (const [key, entry] of Object.entries(value)) {
296
+ if (key !== "input_dir" && key !== "output_file" && key !== "base_path") {
297
+ throw new Error(`Unknown asset config option "${key}" in ${filePath}.`);
298
+ }
299
+ if (typeof entry !== "string" || entry.trim() === "") {
300
+ throw new Error(`Asset config option "${key}" must be a non-empty string.`);
301
+ }
302
+ if (key === "input_dir") config.inputDir = entry;
303
+ if (key === "output_file") config.outputFile = entry;
304
+ if (key === "base_path") config.basePath = entry;
305
+ }
306
+ return config;
307
+ }
308
+ function isNodeError2(error) {
309
+ return error instanceof Error && "code" in error;
310
+ }
311
+ main().catch((error) => {
312
+ console.error(`valfuse-assets: ${error instanceof Error ? error.message : String(error)}`);
313
+ process.exitCode = 1;
314
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DEFAULT_ASSET_GENERATOR_CONFIG: () => DEFAULT_ASSET_GENERATOR_CONFIG,
24
+ generateAssets: () => generateAssets
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/generator.ts
29
+ var import_promises = require("fs/promises");
30
+ var import_node_path = require("path");
31
+ var DEFAULT_ASSET_GENERATOR_CONFIG = Object.freeze({
32
+ inputDir: "public/assets",
33
+ outputFile: "src/assets/assets.ts",
34
+ basePath: "/assets"
35
+ });
36
+ var IGNORED_SYSTEM_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
37
+ async function generateAssets(options = {}) {
38
+ const cwd = (0, import_node_path.resolve)(options.cwd ?? process.cwd());
39
+ const config = normalizeConfig(options.config);
40
+ const inputDir = (0, import_node_path.resolve)(cwd, config.inputDir);
41
+ const outputFile = (0, import_node_path.resolve)(cwd, config.outputFile);
42
+ let inputStats;
43
+ try {
44
+ inputStats = await (0, import_promises.lstat)(inputDir);
45
+ } catch (error) {
46
+ if (isNodeError(error) && error.code === "ENOENT") {
47
+ throw new Error(`Asset input directory not found: ${inputDir}`, { cause: error });
48
+ }
49
+ throw error;
50
+ }
51
+ if (!inputStats.isDirectory()) {
52
+ throw new Error(`Asset input must be a directory and cannot be a symlink: ${inputDir}`);
53
+ }
54
+ const files = await collectAssetFiles(inputDir, outputFile);
55
+ const tree = createAssetTree(files, config.basePath);
56
+ const source = renderAssetModule(tree);
57
+ const existingSource = await readExistingFile(outputFile);
58
+ const changed = existingSource !== source;
59
+ if (options.check) {
60
+ if (changed) {
61
+ throw new Error(
62
+ `Generated asset registry is missing or out of date: ${(0, import_node_path.relative)(cwd, outputFile)}. Run "npx valfuse-assets" to regenerate it.`
63
+ );
64
+ }
65
+ return { assetCount: files.length, outputFile, changed: false };
66
+ }
67
+ if (changed) {
68
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(outputFile), { recursive: true });
69
+ await (0, import_promises.writeFile)(outputFile, source, "utf8");
70
+ }
71
+ return { assetCount: files.length, outputFile, changed };
72
+ }
73
+ function normalizeConfig(input = {}) {
74
+ const config = { ...DEFAULT_ASSET_GENERATOR_CONFIG, ...input };
75
+ for (const [key, value] of Object.entries(config)) {
76
+ if (typeof value !== "string" || value.trim() === "") {
77
+ throw new Error(`Config "${key}" must be a non-empty string.`);
78
+ }
79
+ }
80
+ if (/[?#\\]/.test(config.basePath)) {
81
+ throw new Error('Config "basePath" cannot contain a query, fragment, or backslash.');
82
+ }
83
+ if (!config.basePath.startsWith("/") && !/^https?:\/\//i.test(config.basePath)) {
84
+ throw new Error('Config "basePath" must start with "/" or be an absolute HTTP(S) URL.');
85
+ }
86
+ if (/^https?:\/\//i.test(config.basePath)) {
87
+ const url = new URL(config.basePath);
88
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
89
+ throw new Error('Config "basePath" must use HTTP or HTTPS.');
90
+ }
91
+ }
92
+ if ((0, import_node_path.extname)(config.outputFile).toLowerCase() !== ".ts") {
93
+ throw new Error('Config "outputFile" must have a .ts extension.');
94
+ }
95
+ return config;
96
+ }
97
+ async function collectAssetFiles(inputDir, outputFile) {
98
+ const files = [];
99
+ async function visit(directory, relativeDirectory) {
100
+ const entries = await (0, import_promises.readdir)(directory, { withFileTypes: true });
101
+ entries.sort((left, right) => compareNames(left.name, right.name));
102
+ for (const entry of entries) {
103
+ if (IGNORED_SYSTEM_FILES.has(entry.name)) continue;
104
+ const absolutePath = (0, import_node_path.resolve)(directory, entry.name);
105
+ const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
106
+ if (entry.isDirectory()) {
107
+ await visit(absolutePath, relativePath);
108
+ continue;
109
+ }
110
+ if (!entry.isFile() || pathsEqual(absolutePath, outputFile)) continue;
111
+ files.push({ relativePath });
112
+ }
113
+ }
114
+ await visit(inputDir, "");
115
+ return files;
116
+ }
117
+ function createAssetTree(files, basePath) {
118
+ const root = { originalName: "", children: /* @__PURE__ */ new Map() };
119
+ const normalizedBasePath = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
120
+ for (const file of files) {
121
+ const segments = file.relativePath.split("/");
122
+ const url = buildAssetUrl(normalizedBasePath, segments);
123
+ let parent = root;
124
+ for (let index = 0; index < segments.length; index += 1) {
125
+ const originalName = segments[index];
126
+ const isFile = index === segments.length - 1;
127
+ const key = toAssetKey(originalName, isFile);
128
+ let child = parent.children.get(key);
129
+ if (child && child.originalName !== originalName) {
130
+ throw new Error(
131
+ `Asset names "${child.originalName}" and "${originalName}" both map to "${key}". Rename one path segment so generated keys stay unique.`
132
+ );
133
+ }
134
+ if (!child) {
135
+ child = { originalName, children: /* @__PURE__ */ new Map() };
136
+ parent.children.set(key, child);
137
+ }
138
+ if (isFile) {
139
+ if (child.url !== void 0 || child.children.size > 0) {
140
+ throw new Error(
141
+ `Asset path "${file.relativePath}" collides with another generated asset key.`
142
+ );
143
+ }
144
+ child.url = url;
145
+ } else if (child.url !== void 0) {
146
+ throw new Error(
147
+ `Asset directory "${file.relativePath}" collides with another generated asset key.`
148
+ );
149
+ }
150
+ parent = child;
151
+ }
152
+ }
153
+ return root;
154
+ }
155
+ function toAssetKey(pathSegment, isFile) {
156
+ const name = isFile ? pathSegment.slice(0, pathSegment.length - (0, import_node_path.extname)(pathSegment).length) : pathSegment;
157
+ const normalized = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
158
+ const words = normalized.match(/[A-Za-z0-9]+/g);
159
+ if (!words || words.length === 0) {
160
+ throw new Error(`Asset path segment "${pathSegment}" cannot be converted to a TypeScript key.`);
161
+ }
162
+ const [first, ...rest] = words;
163
+ const key = first.toLowerCase() + rest.map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
164
+ return /^[0-9]/.test(key) ? `_${key}` : key;
165
+ }
166
+ function buildAssetUrl(basePath, segments) {
167
+ const encodedPath = segments.map((segment) => encodeURIComponent(segment)).join("/");
168
+ return basePath ? `${basePath}/${encodedPath}` : `/${encodedPath}`;
169
+ }
170
+ function renderAssetModule(root) {
171
+ const lines = [
172
+ "/**",
173
+ " * Generated by @valfuse-node/assets. Do not edit manually.",
174
+ " */",
175
+ `export const assets = ${renderObject(root, 0)} as const;`,
176
+ "",
177
+ "type AssetLeaves<T> = T extends string",
178
+ " ? T",
179
+ " : T extends Readonly<Record<string, unknown>>",
180
+ " ? AssetLeaves<T[keyof T]>",
181
+ " : never;",
182
+ "",
183
+ "export type AssetPath = AssetLeaves<typeof assets>;",
184
+ "",
185
+ "export default assets;",
186
+ ""
187
+ ];
188
+ return lines.join("\n");
189
+ }
190
+ function renderObject(node, depth) {
191
+ const children = [...node.children.entries()].sort(
192
+ ([left], [right]) => compareNames(left, right)
193
+ );
194
+ if (children.length === 0) return "{}";
195
+ const indent = " ".repeat(depth);
196
+ const childIndent = " ".repeat(depth + 1);
197
+ const properties = children.map(([key, child]) => {
198
+ const value = child.url === void 0 ? renderObject(child, depth + 1) : JSON.stringify(child.url);
199
+ return `${childIndent}[${JSON.stringify(key)}]: ${value},`;
200
+ });
201
+ return `({
202
+ ${properties.join("\n")}
203
+ ${indent}})`;
204
+ }
205
+ async function readExistingFile(filePath) {
206
+ try {
207
+ return await (0, import_promises.readFile)(filePath, "utf8");
208
+ } catch (error) {
209
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
210
+ throw error;
211
+ }
212
+ }
213
+ function pathsEqual(left, right) {
214
+ const resolvedLeft = (0, import_node_path.resolve)(left);
215
+ const resolvedRight = (0, import_node_path.resolve)(right);
216
+ return process.platform === "win32" ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
217
+ }
218
+ function compareNames(left, right) {
219
+ return left < right ? -1 : left > right ? 1 : 0;
220
+ }
221
+ function isNodeError(error) {
222
+ return error instanceof Error && "code" in error;
223
+ }
224
+ // Annotate the CommonJS export names for ESM import in node:
225
+ 0 && (module.exports = {
226
+ DEFAULT_ASSET_GENERATOR_CONFIG,
227
+ generateAssets
228
+ });
@@ -0,0 +1,22 @@
1
+ interface AssetGeneratorConfig {
2
+ inputDir: string;
3
+ outputFile: string;
4
+ basePath: string;
5
+ }
6
+ type AssetGeneratorConfigInput = Partial<AssetGeneratorConfig>;
7
+ interface GenerateAssetsOptions {
8
+ cwd?: string;
9
+ config?: AssetGeneratorConfigInput;
10
+ check?: boolean;
11
+ }
12
+ interface GenerateAssetsResult {
13
+ assetCount: number;
14
+ outputFile: string;
15
+ changed: boolean;
16
+ }
17
+
18
+ declare const DEFAULT_ASSET_GENERATOR_CONFIG: Readonly<AssetGeneratorConfig>;
19
+ /** Generate a stable, typed registry of asset URLs from a directory tree. */
20
+ declare function generateAssets(options?: GenerateAssetsOptions): Promise<GenerateAssetsResult>;
21
+
22
+ export { type AssetGeneratorConfig, type AssetGeneratorConfigInput, DEFAULT_ASSET_GENERATOR_CONFIG, type GenerateAssetsOptions, type GenerateAssetsResult, generateAssets };
@@ -0,0 +1,22 @@
1
+ interface AssetGeneratorConfig {
2
+ inputDir: string;
3
+ outputFile: string;
4
+ basePath: string;
5
+ }
6
+ type AssetGeneratorConfigInput = Partial<AssetGeneratorConfig>;
7
+ interface GenerateAssetsOptions {
8
+ cwd?: string;
9
+ config?: AssetGeneratorConfigInput;
10
+ check?: boolean;
11
+ }
12
+ interface GenerateAssetsResult {
13
+ assetCount: number;
14
+ outputFile: string;
15
+ changed: boolean;
16
+ }
17
+
18
+ declare const DEFAULT_ASSET_GENERATOR_CONFIG: Readonly<AssetGeneratorConfig>;
19
+ /** Generate a stable, typed registry of asset URLs from a directory tree. */
20
+ declare function generateAssets(options?: GenerateAssetsOptions): Promise<GenerateAssetsResult>;
21
+
22
+ export { type AssetGeneratorConfig, type AssetGeneratorConfigInput, DEFAULT_ASSET_GENERATOR_CONFIG, type GenerateAssetsOptions, type GenerateAssetsResult, generateAssets };
package/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ // src/generator.ts
2
+ import { lstat, mkdir, readFile, readdir, writeFile } from "fs/promises";
3
+ import { dirname, extname, relative, resolve } from "path";
4
+ var DEFAULT_ASSET_GENERATOR_CONFIG = Object.freeze({
5
+ inputDir: "public/assets",
6
+ outputFile: "src/assets/assets.ts",
7
+ basePath: "/assets"
8
+ });
9
+ var IGNORED_SYSTEM_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
10
+ async function generateAssets(options = {}) {
11
+ const cwd = resolve(options.cwd ?? process.cwd());
12
+ const config = normalizeConfig(options.config);
13
+ const inputDir = resolve(cwd, config.inputDir);
14
+ const outputFile = resolve(cwd, config.outputFile);
15
+ let inputStats;
16
+ try {
17
+ inputStats = await lstat(inputDir);
18
+ } catch (error) {
19
+ if (isNodeError(error) && error.code === "ENOENT") {
20
+ throw new Error(`Asset input directory not found: ${inputDir}`, { cause: error });
21
+ }
22
+ throw error;
23
+ }
24
+ if (!inputStats.isDirectory()) {
25
+ throw new Error(`Asset input must be a directory and cannot be a symlink: ${inputDir}`);
26
+ }
27
+ const files = await collectAssetFiles(inputDir, outputFile);
28
+ const tree = createAssetTree(files, config.basePath);
29
+ const source = renderAssetModule(tree);
30
+ const existingSource = await readExistingFile(outputFile);
31
+ const changed = existingSource !== source;
32
+ if (options.check) {
33
+ if (changed) {
34
+ throw new Error(
35
+ `Generated asset registry is missing or out of date: ${relative(cwd, outputFile)}. Run "npx valfuse-assets" to regenerate it.`
36
+ );
37
+ }
38
+ return { assetCount: files.length, outputFile, changed: false };
39
+ }
40
+ if (changed) {
41
+ await mkdir(dirname(outputFile), { recursive: true });
42
+ await writeFile(outputFile, source, "utf8");
43
+ }
44
+ return { assetCount: files.length, outputFile, changed };
45
+ }
46
+ function normalizeConfig(input = {}) {
47
+ const config = { ...DEFAULT_ASSET_GENERATOR_CONFIG, ...input };
48
+ for (const [key, value] of Object.entries(config)) {
49
+ if (typeof value !== "string" || value.trim() === "") {
50
+ throw new Error(`Config "${key}" must be a non-empty string.`);
51
+ }
52
+ }
53
+ if (/[?#\\]/.test(config.basePath)) {
54
+ throw new Error('Config "basePath" cannot contain a query, fragment, or backslash.');
55
+ }
56
+ if (!config.basePath.startsWith("/") && !/^https?:\/\//i.test(config.basePath)) {
57
+ throw new Error('Config "basePath" must start with "/" or be an absolute HTTP(S) URL.');
58
+ }
59
+ if (/^https?:\/\//i.test(config.basePath)) {
60
+ const url = new URL(config.basePath);
61
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
62
+ throw new Error('Config "basePath" must use HTTP or HTTPS.');
63
+ }
64
+ }
65
+ if (extname(config.outputFile).toLowerCase() !== ".ts") {
66
+ throw new Error('Config "outputFile" must have a .ts extension.');
67
+ }
68
+ return config;
69
+ }
70
+ async function collectAssetFiles(inputDir, outputFile) {
71
+ const files = [];
72
+ async function visit(directory, relativeDirectory) {
73
+ const entries = await readdir(directory, { withFileTypes: true });
74
+ entries.sort((left, right) => compareNames(left.name, right.name));
75
+ for (const entry of entries) {
76
+ if (IGNORED_SYSTEM_FILES.has(entry.name)) continue;
77
+ const absolutePath = resolve(directory, entry.name);
78
+ const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
79
+ if (entry.isDirectory()) {
80
+ await visit(absolutePath, relativePath);
81
+ continue;
82
+ }
83
+ if (!entry.isFile() || pathsEqual(absolutePath, outputFile)) continue;
84
+ files.push({ relativePath });
85
+ }
86
+ }
87
+ await visit(inputDir, "");
88
+ return files;
89
+ }
90
+ function createAssetTree(files, basePath) {
91
+ const root = { originalName: "", children: /* @__PURE__ */ new Map() };
92
+ const normalizedBasePath = basePath === "/" ? "" : basePath.replace(/\/+$/, "");
93
+ for (const file of files) {
94
+ const segments = file.relativePath.split("/");
95
+ const url = buildAssetUrl(normalizedBasePath, segments);
96
+ let parent = root;
97
+ for (let index = 0; index < segments.length; index += 1) {
98
+ const originalName = segments[index];
99
+ const isFile = index === segments.length - 1;
100
+ const key = toAssetKey(originalName, isFile);
101
+ let child = parent.children.get(key);
102
+ if (child && child.originalName !== originalName) {
103
+ throw new Error(
104
+ `Asset names "${child.originalName}" and "${originalName}" both map to "${key}". Rename one path segment so generated keys stay unique.`
105
+ );
106
+ }
107
+ if (!child) {
108
+ child = { originalName, children: /* @__PURE__ */ new Map() };
109
+ parent.children.set(key, child);
110
+ }
111
+ if (isFile) {
112
+ if (child.url !== void 0 || child.children.size > 0) {
113
+ throw new Error(
114
+ `Asset path "${file.relativePath}" collides with another generated asset key.`
115
+ );
116
+ }
117
+ child.url = url;
118
+ } else if (child.url !== void 0) {
119
+ throw new Error(
120
+ `Asset directory "${file.relativePath}" collides with another generated asset key.`
121
+ );
122
+ }
123
+ parent = child;
124
+ }
125
+ }
126
+ return root;
127
+ }
128
+ function toAssetKey(pathSegment, isFile) {
129
+ const name = isFile ? pathSegment.slice(0, pathSegment.length - extname(pathSegment).length) : pathSegment;
130
+ const normalized = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2");
131
+ const words = normalized.match(/[A-Za-z0-9]+/g);
132
+ if (!words || words.length === 0) {
133
+ throw new Error(`Asset path segment "${pathSegment}" cannot be converted to a TypeScript key.`);
134
+ }
135
+ const [first, ...rest] = words;
136
+ const key = first.toLowerCase() + rest.map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
137
+ return /^[0-9]/.test(key) ? `_${key}` : key;
138
+ }
139
+ function buildAssetUrl(basePath, segments) {
140
+ const encodedPath = segments.map((segment) => encodeURIComponent(segment)).join("/");
141
+ return basePath ? `${basePath}/${encodedPath}` : `/${encodedPath}`;
142
+ }
143
+ function renderAssetModule(root) {
144
+ const lines = [
145
+ "/**",
146
+ " * Generated by @valfuse-node/assets. Do not edit manually.",
147
+ " */",
148
+ `export const assets = ${renderObject(root, 0)} as const;`,
149
+ "",
150
+ "type AssetLeaves<T> = T extends string",
151
+ " ? T",
152
+ " : T extends Readonly<Record<string, unknown>>",
153
+ " ? AssetLeaves<T[keyof T]>",
154
+ " : never;",
155
+ "",
156
+ "export type AssetPath = AssetLeaves<typeof assets>;",
157
+ "",
158
+ "export default assets;",
159
+ ""
160
+ ];
161
+ return lines.join("\n");
162
+ }
163
+ function renderObject(node, depth) {
164
+ const children = [...node.children.entries()].sort(
165
+ ([left], [right]) => compareNames(left, right)
166
+ );
167
+ if (children.length === 0) return "{}";
168
+ const indent = " ".repeat(depth);
169
+ const childIndent = " ".repeat(depth + 1);
170
+ const properties = children.map(([key, child]) => {
171
+ const value = child.url === void 0 ? renderObject(child, depth + 1) : JSON.stringify(child.url);
172
+ return `${childIndent}[${JSON.stringify(key)}]: ${value},`;
173
+ });
174
+ return `({
175
+ ${properties.join("\n")}
176
+ ${indent}})`;
177
+ }
178
+ async function readExistingFile(filePath) {
179
+ try {
180
+ return await readFile(filePath, "utf8");
181
+ } catch (error) {
182
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
183
+ throw error;
184
+ }
185
+ }
186
+ function pathsEqual(left, right) {
187
+ const resolvedLeft = resolve(left);
188
+ const resolvedRight = resolve(right);
189
+ return process.platform === "win32" ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
190
+ }
191
+ function compareNames(left, right) {
192
+ return left < right ? -1 : left > right ? 1 : 0;
193
+ }
194
+ function isNodeError(error) {
195
+ return error instanceof Error && "code" in error;
196
+ }
197
+ export {
198
+ DEFAULT_ASSET_GENERATOR_CONFIG,
199
+ generateAssets
200
+ };
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@valfuse-node/assets",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Generate a typed TypeScript registry for static asset paths",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "assets",
8
+ "asset-generator",
9
+ "codegen",
10
+ "typescript",
11
+ "cli",
12
+ "valfuse"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.0.0"
16
+ },
17
+ "type": "module",
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "bin": {
29
+ "valfuse-assets": "dist/cli/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup && node -e \"import('node:fs/promises').then(({ chmod }) => chmod('dist/cli/index.js', 0o755))\"",
38
+ "dev": "tsup --watch",
39
+ "lint": "eslint src",
40
+ "typecheck": "tsc --noEmit",
41
+ "clean": "node clean.mjs"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "yaml": "^2.5.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "26.3.0",
51
+ "tsup": "8.5.1",
52
+ "typescript": "6.0.3"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/alpinnz/valfuse-node.git",
57
+ "directory": "packages/assets"
58
+ },
59
+ "homepage": "https://github.com/alpinnz/valfuse-node#readme",
60
+ "bugs": {
61
+ "url": "https://github.com/alpinnz/valfuse-node/issues"
62
+ }
63
+ }