next-imagetools 0.0.0-stage → 0.0.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.
@@ -0,0 +1,300 @@
1
+ // \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ let node_fs_promises = require("node:fs/promises");
23
+ let node_path = require("node:path");
24
+ node_path = __toESM(node_path, 1);
25
+ let node_url = require("node:url");
26
+ let imagetools_core = require("imagetools-core");
27
+ let sharp = require("sharp");
28
+ sharp = __toESM(sharp, 1);
29
+ let node_crypto = require("node:crypto");
30
+ let node_fs = require("node:fs");
31
+ // package.json
32
+ var version = "0.0.0";
33
+ // src/shared.ts
34
+ const toParams = (directives) => directives === void 0 ? void 0 : new URLSearchParams(directives);
35
+ const DEFAULT_CACHE_DIR = "node_modules/.cache/next-imagetools";
36
+ function hash(parts) {
37
+ const h = (0, node_crypto.createHash)("sha1");
38
+ for (const p of parts) h.update(p);
39
+ return h.digest("hex");
40
+ }
41
+ const sweeps = /* @__PURE__ */ new Map();
42
+ /**
43
+ * The cache directory to use, or `undefined` when caching is disabled. With `retention` set, the
44
+ * first call in a process for a directory removes entries that haven't been used for that long;
45
+ * loaders have no end-of-build hook, so this is the one point every worker passes through.
46
+ */
47
+ async function prepareCache(ctx, options) {
48
+ const { enabled = true, dir = DEFAULT_CACHE_DIR, retention } = options.cache ?? {};
49
+ if (!enabled) return void 0;
50
+ const resolved = node_path.default.resolve(ctx.rootContext ?? process.cwd(), dir);
51
+ (0, node_fs.mkdirSync)(resolved, { recursive: true });
52
+ if (retention !== void 0) {
53
+ let sweep = sweeps.get(resolved);
54
+ if (!sweep) sweeps.set(resolved, sweep = removeExpired(resolved, retention));
55
+ await sweep;
56
+ }
57
+ return resolved;
58
+ }
59
+ async function removeExpired(dir, retentionSeconds) {
60
+ const cutoff = Date.now() - retentionSeconds * 1e3;
61
+ const names = await (0, node_fs_promises.readdir)(dir);
62
+ await Promise.all(names.map(async (name) => {
63
+ const file = node_path.default.join(dir, name);
64
+ const info = await (0, node_fs_promises.stat)(file).catch(() => void 0);
65
+ if (info?.isFile() && info.mtimeMs < cutoff) await (0, node_fs_promises.rm)(file, { force: true });
66
+ }));
67
+ }
68
+ /**
69
+ * A cached encoding, or `undefined` to process the image again. Any read failure counts as a miss —
70
+ * including an entry another worker's retention sweep removed a moment ago.
71
+ */
72
+ async function readCached(file) {
73
+ try {
74
+ const data = await (0, node_fs_promises.readFile)(file);
75
+ if (data.length === 0) return void 0;
76
+ const now = /* @__PURE__ */ new Date();
77
+ await (0, node_fs_promises.utimes)(file, now, now).catch(() => {});
78
+ return data;
79
+ } catch {
80
+ return;
81
+ }
82
+ }
83
+ async function writeCache(file, data) {
84
+ const tmp = `${file}.${process.pid}-${Math.random().toString(36).slice(2)}.tmp`;
85
+ try {
86
+ await (0, node_fs_promises.writeFile)(tmp, data);
87
+ await (0, node_fs_promises.rename)(tmp, file);
88
+ } catch {
89
+ await (0, node_fs_promises.rm)(tmp, { force: true });
90
+ }
91
+ }
92
+ const inflight = /* @__PURE__ */ new Map();
93
+ /**
94
+ * Everything besides the config and the source that decides the encoded bytes: this package
95
+ * (transform behavior), and sharp with every codec it bundles (`sharp.versions` lists libvips,
96
+ * mozjpeg, libwebp, aom, …). Upgrading any of them must not serve images encoded by the old one.
97
+ */
98
+ const ENGINE = JSON.stringify({
99
+ package: version,
100
+ sharp: sharp.default.versions
101
+ });
102
+ function cacheKey(config, sourceHash, removeMetadata, engine = ENGINE) {
103
+ return hash([
104
+ JSON.stringify(config),
105
+ sourceHash,
106
+ String(removeMetadata),
107
+ engine
108
+ ]);
109
+ }
110
+ /**
111
+ * Runs one resolved config through imagetools-core, memoized in-process and on disk. Both the
112
+ * module loader (needs dimensions) and the asset loader (needs bytes) call this. The cache key
113
+ * covers the source, the config, the options that change the output and the engine versions, so
114
+ * editing any of them never hits a stale entry.
115
+ */
116
+ function processImage(args) {
117
+ const engine = args.configHash ? `${ENGINE}|config:${args.configHash}` : ENGINE;
118
+ const id = cacheKey(args.config, args.sourceHash, args.removeMetadata, engine);
119
+ let p = inflight.get(id);
120
+ if (!p) {
121
+ p = run(id, args);
122
+ inflight.set(id, p);
123
+ p.finally(() => inflight.delete(id)).catch(() => {});
124
+ }
125
+ return p;
126
+ }
127
+ async function run(id, args) {
128
+ const file = args.cacheDir === void 0 ? void 0 : node_path.default.join(args.cacheDir, id);
129
+ let data = file === void 0 ? void 0 : await readCached(file);
130
+ let transforms = {};
131
+ if (!data) {
132
+ const { transforms: steps } = (0, imagetools_core.generateTransforms)(args.config, args.factories, args.searchParams, args.logger);
133
+ const animation = await animationOf(args.source, args.config);
134
+ const input = (0, sharp.default)(args.source, { animated: animation.kept });
135
+ const res = await (0, imagetools_core.applyTransforms)(animation.kept ? [frameSize(animation.frameHeight), ...steps] : steps, input, args.removeMetadata);
136
+ data = (await res.image.toBuffer({ resolveWithObject: true })).data;
137
+ transforms = res.metadata.transforms;
138
+ if (file !== void 0) await writeCache(file, data);
139
+ }
140
+ const meta = await (0, sharp.default)(data).metadata();
141
+ const format = args.config["format"] ?? transforms.format ?? meta.format;
142
+ if (format !== void 0) transforms.format = format;
143
+ return {
144
+ id,
145
+ data,
146
+ meta,
147
+ transforms
148
+ };
149
+ }
150
+ const NO_CONFIG = {
151
+ config: {},
152
+ hash: "",
153
+ factories: imagetools_core.builtins
154
+ };
155
+ const loadedConfigs = /* @__PURE__ */ new Map();
156
+ /**
157
+ * Imports the `configFile` module, if any. Three caches could otherwise serve an outdated config:
158
+ *
159
+ * - Turbopack, which reruns a loader only when its inputs change: the file is registered as a
160
+ * dependency of every module that uses it.
161
+ * - Node's module cache, which returns the first import forever: the file is imported with its
162
+ * content hash in the URL, so an edit loads a new module.
163
+ * - The disk cache: the hash is part of every cache key (see processImage).
164
+ */
165
+ async function loadConfig(ctx, options) {
166
+ if (!options.configFile) return NO_CONFIG;
167
+ const file = node_path.default.resolve(ctx.rootContext ?? process.cwd(), options.configFile);
168
+ ctx.addDependency?.(file);
169
+ const contentHash = hash([await (0, node_fs_promises.readFile)(file).catch(() => {
170
+ throw new Error(`next-imagetools: configFile not found at ${file}`);
171
+ })]);
172
+ let loaded = loadedConfigs.get(contentHash);
173
+ if (!loaded) {
174
+ loaded = import(`${(0, node_url.pathToFileURL)(file).href}?v=${contentHash}`).then((mod) => {
175
+ const config = mod.default ?? mod;
176
+ return {
177
+ config,
178
+ hash: contentHash,
179
+ factories: config.extendTransforms?.(imagetools_core.builtins) ?? imagetools_core.builtins
180
+ };
181
+ });
182
+ loadedConfigs.set(contentHash, loaded);
183
+ }
184
+ return loaded;
185
+ }
186
+ /**
187
+ * Runs a loader, prefixing any error with the file and query it was processing: in a large app,
188
+ * "Expected positive integer for width" alone doesn't say which of hundreds of imports to fix.
189
+ */
190
+ async function withContext(ctx, task) {
191
+ try {
192
+ return await task();
193
+ } catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ throw new Error(`next-imagetools: ${where(ctx)}: ${message}`, { cause: error });
196
+ }
197
+ }
198
+ /** The import being processed, as the user wrote it: app-relative path plus query. */
199
+ function where(ctx) {
200
+ return `${node_path.default.relative(ctx.rootContext ?? process.cwd(), ctx.resourcePath).split(node_path.default.sep).join("/")}${ctx.resourceQuery}`;
201
+ }
202
+ /**
203
+ * Emits a build warning about the current import. Turbopack prints a warning's name and stack, so
204
+ * the stack is replaced by the message: it's about the user's import, not our code.
205
+ */
206
+ function warn(ctx, message) {
207
+ const warning = /* @__PURE__ */ new Error(`${where(ctx)}: ${message}`);
208
+ warning.name = "next-imagetools";
209
+ warning.stack = `${warning.name}: ${warning.message}`;
210
+ ctx.emitWarning?.(warning);
211
+ }
212
+ /** Output formats sharp can encode with several frames. */
213
+ const ANIMATED_FORMATS = /* @__PURE__ */ new Set(["gif", "webp"]);
214
+ /**
215
+ * Whether a source is an animation. Several pages alone don't make one: a multi-page TIFF has them
216
+ * too, and must not turn into an animated WebP. Only animations carry frame delays.
217
+ */
218
+ const isAnimated = (meta) => (meta.pages ?? 1) > 1 && meta.delay !== void 0;
219
+ /**
220
+ * Whether an image keeps its animation — an animated source, and an output that can hold the frames
221
+ * — and the height of one frame.
222
+ */
223
+ async function animationOf(source, config) {
224
+ const meta = await (0, sharp.default)(source).metadata();
225
+ const format = config["format"] ?? meta.format;
226
+ return {
227
+ kept: isAnimated(meta) && format !== void 0 && ANIMATED_FORMATS.has(format),
228
+ frameHeight: meta.height
229
+ };
230
+ }
231
+ /**
232
+ * An animated image is read as one tall strip of frames, so the pipeline would start from the
233
+ * strip's height and resize every frame to the wrong aspect ratio. This first step puts one frame's
234
+ * height in place; sharp then applies the resize to each frame.
235
+ */
236
+ const frameSize = (frameHeight) => (state, image) => {
237
+ state.info.height = frameHeight;
238
+ state.info.autoOriented.height = frameHeight;
239
+ return image;
240
+ };
241
+ Object.defineProperty(exports, "ANIMATED_FORMATS", {
242
+ enumerable: true,
243
+ get: function() {
244
+ return ANIMATED_FORMATS;
245
+ }
246
+ });
247
+ Object.defineProperty(exports, "__toESM", {
248
+ enumerable: true,
249
+ get: function() {
250
+ return __toESM;
251
+ }
252
+ });
253
+ Object.defineProperty(exports, "hash", {
254
+ enumerable: true,
255
+ get: function() {
256
+ return hash;
257
+ }
258
+ });
259
+ Object.defineProperty(exports, "isAnimated", {
260
+ enumerable: true,
261
+ get: function() {
262
+ return isAnimated;
263
+ }
264
+ });
265
+ Object.defineProperty(exports, "loadConfig", {
266
+ enumerable: true,
267
+ get: function() {
268
+ return loadConfig;
269
+ }
270
+ });
271
+ Object.defineProperty(exports, "prepareCache", {
272
+ enumerable: true,
273
+ get: function() {
274
+ return prepareCache;
275
+ }
276
+ });
277
+ Object.defineProperty(exports, "processImage", {
278
+ enumerable: true,
279
+ get: function() {
280
+ return processImage;
281
+ }
282
+ });
283
+ Object.defineProperty(exports, "toParams", {
284
+ enumerable: true,
285
+ get: function() {
286
+ return toParams;
287
+ }
288
+ });
289
+ Object.defineProperty(exports, "warn", {
290
+ enumerable: true,
291
+ get: function() {
292
+ return warn;
293
+ }
294
+ });
295
+ Object.defineProperty(exports, "withContext", {
296
+ enumerable: true,
297
+ get: function() {
298
+ return withContext;
299
+ }
300
+ });
package/package.json CHANGED
@@ -1,6 +1,111 @@
1
1
  {
2
2
  "name": "next-imagetools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "Build-time image transforms for Next.js: import './a.jpg?w=400;800&format=webp&as=srcset' and get optimized files, no image server needed.",
5
+ "keywords": [
6
+ "avif",
7
+ "imagetools",
8
+ "next",
9
+ "nextjs",
10
+ "responsive-images",
11
+ "sharp",
12
+ "srcset",
13
+ "static-export",
14
+ "turbopack",
15
+ "webp"
16
+ ],
17
+ "homepage": "https://github.com/awdr74100/next-imagetools#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/awdr74100/next-imagetools/issues"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Roya <a78945612385238@gmail.com>",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/awdr74100/next-imagetools.git"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "client.d.ts"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.mjs",
35
+ "types": "./dist/index.d.mts",
36
+ "exports": {
37
+ ".": {
38
+ "types": {
39
+ "import": "./dist/index.d.mts",
40
+ "require": "./dist/index.d.cts"
41
+ },
42
+ "import": "./dist/index.mjs",
43
+ "require": "./dist/index.cjs"
44
+ },
45
+ "./config": {
46
+ "types": {
47
+ "import": "./dist/config.d.mts",
48
+ "require": "./dist/config.d.cts"
49
+ },
50
+ "import": "./dist/config.mjs",
51
+ "require": "./dist/config.cjs"
52
+ },
53
+ "./core": {
54
+ "types": {
55
+ "import": "./dist/core.d.mts",
56
+ "require": "./dist/core.d.cts"
57
+ },
58
+ "import": "./dist/core.mjs",
59
+ "require": "./dist/core.cjs"
60
+ },
61
+ "./loader": "./dist/loader.cjs",
62
+ "./asset-loader": "./dist/asset-loader.cjs",
63
+ "./client": {
64
+ "types": "./client.d.ts"
65
+ },
66
+ "./package.json": "./package.json"
67
+ },
68
+ "dependencies": {
69
+ "imagetools-core": "^12.0.0",
70
+ "picomatch": "^4.0.7",
71
+ "sharp": "^0.35.5"
72
+ },
73
+ "devDependencies": {
74
+ "@types/node": "^26.6.6",
75
+ "@types/picomatch": "^4.0.3",
76
+ "changelogen": "^0.6.2",
77
+ "knip": "^6.44.2",
78
+ "next": "^16.4.5",
79
+ "oxfmt": "^0.78.0",
80
+ "oxlint": "^1.93.0",
81
+ "playwright": "^1.65.0",
82
+ "publint": "^0.3.25",
83
+ "tsdown": "^0.23.7",
84
+ "typescript": "^6.0.3",
85
+ "vitest": "^5.1.0"
86
+ },
87
+ "peerDependencies": {
88
+ "next": ">=16.2.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=22.12.0"
92
+ },
93
+ "scripts": {
94
+ "build": "tsdown",
95
+ "build:watch": "tsdown --watch",
96
+ "check": "pnpm typecheck && pnpm lint && pnpm lint:english && pnpm format:check && pnpm knip",
97
+ "format": "oxfmt .",
98
+ "format:check": "oxfmt --check .",
99
+ "knip": "knip",
100
+ "knip:fix": "knip --fix",
101
+ "lint": "oxlint --deny-warnings .",
102
+ "lint:english": "node scripts/check-english.mjs",
103
+ "lint:fix": "oxlint --fix .",
104
+ "playground": "pnpm build && pnpm -C playground run dev",
105
+ "release": "node scripts/release.mjs",
106
+ "test": "vitest run",
107
+ "test:e2e": "vitest run --project e2e",
108
+ "test:unit": "vitest run --project unit",
109
+ "typecheck": "pnpm -r run typecheck && tsc --noEmit -p tsconfig.json"
110
+ }
6
111
  }