From adad84d3aba7c97d1a2359b75f212973f2c77968 Mon Sep 17 00:00:00 2001
From: Artyom Sovetnikov <2056864+elringus@users.noreply.github.com>
Date: Thu, 8 Feb 2024 16:00:47 +0300
Subject: [PATCH] add asset import (#15)
---
docs/.vitepress/config.ts | 1 +
docs/guide/asset-import.md | 48 +++++++++++++++++++++++++++
docs/guide/getting-started.md | 2 +-
docs/guide/integrations/astro.md | 49 +++++++++++++++++++++++++++-
docs/guide/integrations/nuxt.md | 2 +-
docs/guide/integrations/remix.md | 2 +-
docs/guide/integrations/solid.md | 2 +-
docs/guide/integrations/svelte.md | 2 +-
docs/guide/integrations/vite.md | 2 +-
docs/guide/integrations/vitepress.md | 2 +-
docs/package.json | 2 +-
package.json | 8 ++---
samples/astro/README.md | 2 +-
samples/astro/package.json | 4 +--
samples/astro/src/env.d.ts | 1 +
samples/astro/src/pages/import.astro | 31 ++++++++++++++++++
samples/astro/src/pages/index.astro | 2 ++
scripts/build.sh | 1 +
src/client.d.ts | 5 +++
src/plugin/vite.ts | 10 ++++--
src/server/import.ts | 46 ++++++++++++++++++++++++++
src/server/index.ts | 1 +
src/server/transform/5-encode.ts | 4 +--
src/server/transform/6-build.ts | 22 ++++++++-----
src/server/transform/index.ts | 8 ++---
src/tsconfig.json | 2 --
test/server/import.spec.ts | 45 +++++++++++++++++++++++++
test/server/vite.spec.ts | 26 +++++++++++++++
28 files changed, 297 insertions(+), 35 deletions(-)
create mode 100644 docs/guide/asset-import.md
create mode 100644 samples/astro/src/pages/import.astro
create mode 100644 src/client.d.ts
create mode 100644 src/server/import.ts
create mode 100644 test/server/import.spec.ts
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 1fe3503..1e77ee2 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -55,6 +55,7 @@ export default defineConfig({
items: [
{ text: "Introduction", link: "/guide/" },
{ text: "Getting Started", link: "/guide/getting-started" },
+ { text: "Asset Import", link: "/guide/asset-import" },
{ text: "GPU Acceleration", link: "/guide/gpu-acceleration" },
{ text: "Plugins", link: "/guide/plugins" }
]
diff --git a/docs/guide/asset-import.md b/docs/guide/asset-import.md
new file mode 100644
index 0000000..fe9e593
--- /dev/null
+++ b/docs/guide/asset-import.md
@@ -0,0 +1,48 @@
+# Asset Import
+
+By default, imgit is set up to detect and transform Markdown syntax in the source content. This works best for simple documentation and blog websites, but may not be flexible enough for more complex apps authored with frameworks like React.
+
+To better fit component-based apps, imgit allows importing media assets with `import` statement to manually author the desired HTML.
+
+Use `imgit:` namespace when importing a media asset to make imgit optimize it and return sources of the generated assets. For example, consider following [Astro](https://astro.build) page:
+
+```astro
+---
+import psd from "imgit:https://example.com/photo.psd";
+import mkv from "imgit:/assets/video.mkv";
+---
+
+
+
+
+```
+
+Imported asset returns following default export:
+
+```ts
+type AssetImport = {
+ content: {
+ encoded: string,
+ dense?: string,
+ cover?: string,
+ safe?: string
+ },
+ info: {
+ type: string,
+ height: number,
+ width: number,
+ alpha: boolean
+ }
+};
+```
+
+— where `content` are the sources of the generated optimized files, which you can assign to the various `src` attributes of the built HTML. Additional `info` object contains metadata describing the imported asset, such its dimensions and MIME type, which may be helpful when building the host component.
+
+::: tip
+When using TypeScript, add `/// ` to a `.d.ts` file anywhere under project source directory to correctly resolve virtual asset imports.
+:::
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 54bee82..df77f13 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -115,7 +115,7 @@ await fs.writeFile("./public/index.html", output);
await exit();
```
-::: tip Example
+::: tip SAMPLE
Find minimal sample on using imgit directly with Deno runtime on GitHub: https://github.com/elringus/imgit/tree/main/samples/minimal.
:::
diff --git a/docs/guide/integrations/astro.md b/docs/guide/integrations/astro.md
index 03ac2b0..b3645ec 100644
--- a/docs/guide/integrations/astro.md
+++ b/docs/guide/integrations/astro.md
@@ -25,6 +25,53 @@ export default defineConfig({
:::
-::: tip Sample
+When building the project, imgit will automatically transform image Markdown syntax
+into optimized HTML. For example, given following `index.md` page:
+
+```md
+# PSD Image
+![](https://example.com/photo.psd)
+
+# MKV Video
+![](/assets/video.mkv)
+
+# YouTube Video
+![](https://www.youtube.com/watch?v=arbuYnJoLtU)
+```
+
+— imgit will produce following HTML output:
+
+```html
+
PSD Image
+
+
+MKV Video
+
+
+YouTube Video
+optimized YouTube player
+```
+
+In case you'd like to instead manually build the HTML (eg, with custom components), import the media assets with `imgit:` namespace:
+
+```astro
+---
+import psd from "imgit:https://example.com/photo.psd";
+import mkv from "imgit:/assets/video.mkv";
+---
+
+
+
+
+```
+
+When using TypeScript, add `/// ` to a `.d.ts` file anywhere inside project source directory to correctly resolve virtual asset imports.
+
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/astro
:::
diff --git a/docs/guide/integrations/nuxt.md b/docs/guide/integrations/nuxt.md
index 89ee557..45680cf 100644
--- a/docs/guide/integrations/nuxt.md
+++ b/docs/guide/integrations/nuxt.md
@@ -23,6 +23,6 @@ export default defineNuxtConfig({
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/nuxt
:::
diff --git a/docs/guide/integrations/remix.md b/docs/guide/integrations/remix.md
index 7cfad16..db2c654 100644
--- a/docs/guide/integrations/remix.md
+++ b/docs/guide/integrations/remix.md
@@ -26,6 +26,6 @@ export default defineConfig({
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/remix
:::
diff --git a/docs/guide/integrations/solid.md b/docs/guide/integrations/solid.md
index d2fbbb1..634a97f 100644
--- a/docs/guide/integrations/solid.md
+++ b/docs/guide/integrations/solid.md
@@ -26,6 +26,6 @@ export default defineConfig({
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/solid
:::
diff --git a/docs/guide/integrations/svelte.md b/docs/guide/integrations/svelte.md
index a31835b..085bd93 100644
--- a/docs/guide/integrations/svelte.md
+++ b/docs/guide/integrations/svelte.md
@@ -40,6 +40,6 @@ export default defineConfig({
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/svelte
:::
diff --git a/docs/guide/integrations/vite.md b/docs/guide/integrations/vite.md
index e374661..ef8ed3f 100644
--- a/docs/guide/integrations/vite.md
+++ b/docs/guide/integrations/vite.md
@@ -25,6 +25,6 @@ export default defineConfig({
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/vite
:::
diff --git a/docs/guide/integrations/vitepress.md b/docs/guide/integrations/vitepress.md
index a946014..841438c 100644
--- a/docs/guide/integrations/vitepress.md
+++ b/docs/guide/integrations/vitepress.md
@@ -42,6 +42,6 @@ export default { extends: { Layout: DefaultTheme.Layout } };
:::
-::: tip Sample
+::: tip SAMPLE
https://github.com/elringus/imgit/tree/main/samples/vitepress
:::
diff --git a/docs/package.json b/docs/package.json
index ce906b3..12c78ad 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -10,6 +10,6 @@
"typescript": "^5.3.3",
"vitepress": "^1.0.0-rc.42",
"typedoc-vitepress-theme": "^1.0.0-next.9",
- "imgit": "^0.1.3"
+ "imgit": "^0.2.1"
}
}
diff --git a/package.json b/package.json
index 4bf51cf..13d8ea0 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "imgit",
- "version": "0.1.3",
+ "version": "0.2.1",
"description": "Transform images, video and YouTube links to HTML optimized for web vitals.",
"author": "Elringus (https://elringus.me)",
"license": "MIT",
"keywords": ["CLS", "lazy-load", "embed", "size", "encode", "compress", "md", "avif", "vite-plugin"],
- "repository": { "type": "git", "url": "https://github.com/elringus/imgit.git" },
+ "repository": { "type": "git", "url": "git+https://github.com/elringus/imgit.git" },
"funding": "https://github.com/sponsors/elringus",
"homepage": "https://imgit.dev",
"bugs": { "url": "https://github.com/elringus/imgit/issues" },
@@ -31,7 +31,7 @@
},
"devDependencies": {
"typescript": "^5.3.3",
- "vitest": "^1.1.3",
- "@vitest/coverage-v8": "^1.1.3"
+ "vitest": "^1.2.2",
+ "@vitest/coverage-v8": "^1.2.2"
}
}
diff --git a/samples/astro/README.md b/samples/astro/README.md
index 8d03f53..d136a69 100644
--- a/samples/astro/README.md
+++ b/samples/astro/README.md
@@ -10,4 +10,4 @@ Example on plugging imgit to [astro](https://astro.build) web framework:
> [!IMPORTANT]
> Initial build could take up to 5 minutes for all the sample assets referenced in index.astro to fetch and encode. The files will be stored under `public` directory and consequent runs won't incur additional processing time.
-Examine `src/pages/index.astro` and `astro.config.mts` sources for details.
+Examine `src/pages/index.astro` (markdown source transform), `src/pages/import.astro` (manual asset import) and `astro.config.mts` sources for details.
diff --git a/samples/astro/package.json b/samples/astro/package.json
index 31e02e2..aa7a37e 100644
--- a/samples/astro/package.json
+++ b/samples/astro/package.json
@@ -5,7 +5,7 @@
"preview": "astro preview"
},
"dependencies": {
- "astro": "^4.1.1",
- "imgit": "^0.1.2"
+ "astro": "^4.3.5",
+ "imgit": "^0.2.1"
}
}
diff --git a/samples/astro/src/env.d.ts b/samples/astro/src/env.d.ts
index f964fe0..8f1c277 100644
--- a/samples/astro/src/env.d.ts
+++ b/samples/astro/src/env.d.ts
@@ -1 +1,2 @@
///
+///
diff --git a/samples/astro/src/pages/import.astro b/samples/astro/src/pages/import.astro
new file mode 100644
index 0000000..0965c54
--- /dev/null
+++ b/samples/astro/src/pages/import.astro
@@ -0,0 +1,31 @@
+---
+import psd from "imgit:https://github.com/elringus/imgit/raw/main/samples/assets/psd.psd";
+import mkv from "imgit:https://github.com/elringus/imgit/raw/main/samples/assets/mkv.mkv";
+---
+
+
+
+
+ Astro Import Sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/astro/src/pages/index.astro b/samples/astro/src/pages/index.astro
index b07bf83..6f02c3b 100644
--- a/samples/astro/src/pages/index.astro
+++ b/samples/astro/src/pages/index.astro
@@ -12,6 +12,8 @@
+Import Sample
+
diff --git a/scripts/build.sh b/scripts/build.sh
index 494093b..36b4ddf 100644
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -1,4 +1,5 @@
rm -rf dist
tsc --build src
+cp src/client.d.ts dist
cp src/client/styles.css dist/client
cp src/plugin/youtube/styles.css dist/plugin/youtube
diff --git a/src/client.d.ts b/src/client.d.ts
new file mode 100644
index 0000000..f3f1b3a
--- /dev/null
+++ b/src/client.d.ts
@@ -0,0 +1,5 @@
+/* v8 ignore start */
+declare module "imgit:*" {
+ const asset: import("./server/import.js").AssetImport;
+ export default asset;
+}
diff --git a/src/plugin/vite.ts b/src/plugin/vite.ts
index bebf976..be65fd9 100644
--- a/src/plugin/vite.ts
+++ b/src/plugin/vite.ts
@@ -1,4 +1,4 @@
-import { Platform, Prefs, Plugin, boot, exit, transform, std } from "../server/index.js";
+import { Platform, Prefs, Plugin, boot, exit, transform, std, loader } from "../server/index.js";
/** Configures vite plugin behaviour. */
export type VitePrefs = Prefs & {
@@ -17,8 +17,10 @@ export type VitePlugin = {
transformIndexHtml: {
order: "pre" | "post",
handler: (html: string, ctx: { filename: string }) => Promise<{ html: string, tags: HtmlTag[] }>
- }
+ };
buildEnd: (error?: Error) => Promise | void;
+ resolveId: (source: string) => string | null;
+ load: (id: string) => Promise | null;
};
// https://vitejs.dev/guide/api-plugin#transformindexhtml
@@ -45,7 +47,9 @@ export default function (prefs?: VitePrefs, platform?: Platform): VitePlugin {
tags: !prefs || prefs.inject !== false ? inject(prefs?.plugins) : []
})
},
- buildEnd: exit
+ buildEnd: exit,
+ resolveId: (source) => loader.isImgitAssetImport(source) ? source : null,
+ load: (id) => loader.isImgitAssetImport(id) ? loader.importImgitAsset(id) : null
};
}
diff --git a/src/server/import.ts b/src/server/import.ts
new file mode 100644
index 0000000..20b18d8
--- /dev/null
+++ b/src/server/import.ts
@@ -0,0 +1,46 @@
+import { stages } from "./transform/index.js";
+import { EncodedContent, ContentInfo, BuiltAsset } from "./asset.js";
+
+/** Result of importing asset via imgit. */
+export type AssetImport = {
+ /** Sources of the asset content. */
+ content: EncodedContent;
+ /** Content metadata. */
+ info: ContentInfo;
+}
+
+/** Whether specified import identifier is an imgit asset import. */
+export function isImgitAssetImport(importId: string): boolean {
+ return importId.startsWith("imgit:");
+}
+
+/** Resolves result (source code) of importing an imgit asset. */
+export async function importImgitAsset(importId: string): Promise {
+ const url = importId.substring(6);
+ const asset = { syntax: { text: "", index: -1, url } };
+ stages.resolve.asset(asset);
+ await stages.fetch.asset(asset);
+ await stages.probe.asset(asset);
+ await stages.encode.asset(asset);
+ const size = stages.build.size(asset);
+ return `export default {
+ content: {
+ encoded: ${buildSrc(asset.content.encoded)},
+ dense: ${buildSrc(asset.content.dense)},
+ cover: ${buildSrc(asset.content.cover)},
+ safe: ${buildSrc(asset.content.safe)}
+ },
+ info: {
+ type: "${asset.content.info.type}",
+ height: ${size.height},
+ width: ${size.width},
+ alpha: ${asset.content.info.alpha}
+ }
+ }`;
+}
+
+function buildSrc(path?: string) {
+ if (path === undefined) return "undefined";
+ const src = stages.build.source(path);
+ return `"${src}"`;
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index ec6e82e..56ca223 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,6 +8,7 @@ export { Plugin, PluginInjection } from "./config/plugin.js";
export { ctx } from "./context.js";
export { Cache, cache } from "./cache.js";
export { stages, transform } from "./transform/index.js";
+export * as loader from "./import.js";
export * from "./config/index.js";
export * from "./asset.js";
diff --git a/src/server/transform/5-encode.ts b/src/server/transform/5-encode.ts
index c317767..ddd3cab 100644
--- a/src/server/transform/5-encode.ts
+++ b/src/server/transform/5-encode.ts
@@ -8,12 +8,12 @@ export async function encodeAll(assets: ProbedAsset[]): Promise
await everythingIsFetched();
for (const asset of assets)
if (!(await encodeWithPlugins(asset)))
- await encodeAsset(asset);
+ await encode(asset);
return assets;
}
/** Encodes asset content with ffmpeg. */
-export async function encodeAsset(asset: EncodedAsset): Promise {
+export async function encode(asset: EncodedAsset): Promise {
await encodeMain(asset.content, asset);
await encodeSafe(asset.content, asset);
await encodeDense(asset.content, asset);
diff --git a/src/server/transform/6-build.ts b/src/server/transform/6-build.ts
index 65ca220..46b9799 100644
--- a/src/server/transform/6-build.ts
+++ b/src/server/transform/6-build.ts
@@ -30,11 +30,21 @@ export async function build(asset: BuiltAsset, merges?: BuiltAsset[]): Promise threshold ? threshold / info.width : 1;
+ const width = Math.floor(info.width * mod);
+ const height = Math.floor(info.height * mod);
+ return { width, height };
+}
+
async function mergeAndBuild(asset: BuiltAsset, merges: BuiltAsset[]): Promise {
for (const merge of merges) merge.html = "";
if (!(await buildWithPlugins(asset, merges))) await build(asset, merges);
@@ -120,11 +130,7 @@ async function getCoverData(asset: BuiltAsset, path: string): Promise {
}
function buildSizeAttributes(asset: BuiltAsset): string {
- const info = asset.content.info;
- const threshold = asset.spec.width ?? cfg.width;
- const mod = threshold && info.width > threshold ? threshold / info.width : 1;
- const width = Math.floor(info.width * mod);
- const height = Math.floor(info.height * mod);
+ const { width, height } = resolveSize(asset);
return `width="${width}" height="${height}"`;
}
@@ -134,5 +140,5 @@ async function serve(path: string, asset: BuiltAsset): Promise {
const src = await plugin.serve(path, asset);
if (src) return src;
}
- return buildContentSource(path);
+ return resolveSource(path);
}
diff --git a/src/server/transform/index.ts b/src/server/transform/index.ts
index 2f533e3..c8021b5 100644
--- a/src/server/transform/index.ts
+++ b/src/server/transform/index.ts
@@ -2,8 +2,8 @@ import { captureAll, capture } from "./1-capture.js";
import { resolveAll, resolve, resolveSpec } from "./2-resolve.js";
import { fetchAll, fetch } from "./3-fetch.js";
import { probeAll, probe } from "./4-probe.js";
-import { encodeAll, encodeAsset } from "./5-encode.js";
-import { buildAll, build, buildContentSource, CONTAINER_ATTR } from "./6-build.js";
+import { encodeAll, encode } from "./5-encode.js";
+import { buildAll, build, resolveSource, CONTAINER_ATTR, resolveSize } from "./6-build.js";
import { rewriteAll, rewrite } from "./7-rewrite.js";
/** Individual document transformation stages. */
@@ -12,8 +12,8 @@ export const stages = {
resolve: { asset: resolve, spec: resolveSpec },
fetch: { asset: fetch },
probe: { asset: probe },
- encode: { asset: encodeAsset },
- build: { asset: build, source: buildContentSource, CONTAINER_ATTR },
+ encode: { asset: encode },
+ build: { asset: build, source: resolveSource, size: resolveSize, CONTAINER_ATTR },
rewrite: { content: rewrite }
};
diff --git a/src/tsconfig.json b/src/tsconfig.json
index d4dab6d..cfac382 100644
--- a/src/tsconfig.json
+++ b/src/tsconfig.json
@@ -4,9 +4,7 @@
"module": "NodeNext",
"skipLibCheck": true,
"strict": true,
- "sourceMap": true,
"declaration": true,
- "declarationMap": true,
"outDir": "../dist"
}
}
diff --git a/test/server/import.spec.ts b/test/server/import.spec.ts
new file mode 100644
index 0000000..ec80d5f
--- /dev/null
+++ b/test/server/import.spec.ts
@@ -0,0 +1,45 @@
+import { it, expect, vi } from "vitest";
+import { boot } from "./common.js";
+import { isImgitAssetImport, importImgitAsset } from "../../src/server/import.js";
+import { ContentInfo, EncodedContent } from "../../src/server/index.js";
+
+it("assumes imgit import when starts with imgit:", async () => {
+ expect(isImgitAssetImport("foo")).toBeFalsy();
+ expect(isImgitAssetImport("imgit:foo")).toBeTruthy();
+});
+
+it("invokes build pipeline when importing imgit asset", async () => {
+ const info: ContentInfo = { alpha: true, width: 7, height: 6, type: "foo" };
+ const content: EncodedContent = { info, src: "", encoded: "bar", dense: "baz", local: "" };
+ const asset = { content };
+ vi.spyOn(await import("../../src/server/transform/index.js"), "stages", "get").mockReturnValue({
+ capture: { assets: vi.fn() },
+ resolve: { asset: vi.fn(), spec: vi.fn() },
+ fetch: { asset: vi.fn() },
+ probe: { asset: vi.fn() },
+ encode: { asset: vi.fn(async input => void Object.assign(input, asset)) },
+ build: {
+ asset: vi.fn(),
+ source: vi.fn(path => path),
+ size: vi.fn(() => ({ width: 1, height: 2 })),
+ CONTAINER_ATTR: ""
+ },
+ rewrite: { content: vi.fn() }
+ });
+ await boot();
+ const code = await importImgitAsset("imgit:foo");
+ expect(code).toStrictEqual(`export default {
+ content: {
+ encoded: "bar",
+ dense: "baz",
+ cover: undefined,
+ safe: undefined
+ },
+ info: {
+ type: "foo",
+ height: 2,
+ width: 1,
+ alpha: true
+ }
+ }`);
+});
diff --git a/test/server/vite.spec.ts b/test/server/vite.spec.ts
index 899840d..469af2a 100644
--- a/test/server/vite.spec.ts
+++ b/test/server/vite.spec.ts
@@ -80,3 +80,29 @@ it("doesn't inject client module when disabled in plugin config", async () => {
tags: []
});
});
+
+it("doesn't resolve non-imgit module imports", async () => {
+ vi.spyOn(await import("../../src/server/import.js"), "isImgitAssetImport").mockReturnValue(false);
+ expect(vite().resolveId("foo")).toBeNull();
+});
+
+it("resolves imgit module imports", async () => {
+ vi.spyOn(await import("../../src/server/import.js"), "isImgitAssetImport").mockReturnValue(true);
+ expect(vite().resolveId("foo")).toStrictEqual("foo");
+});
+
+it("doesn't load non-imgit import", async () => {
+ vi.spyOn(await import("../../src/server/import.js"), "isImgitAssetImport").mockReturnValue(false);
+ const load = vi.spyOn(await import("../../src/server/import.js"), "importImgitAsset");
+ load.mockImplementation(() => Promise.reject());
+ await vite().load("");
+ expect(load).not.toBeCalled();
+});
+
+it("loads imgit import", async () => {
+ vi.spyOn(await import("../../src/server/import.js"), "isImgitAssetImport").mockReturnValue(true);
+ const load = vi.spyOn(await import("../../src/server/import.js"), "importImgitAsset");
+ load.mockImplementation(() => Promise.resolve("foo"));
+ await vite().load("");
+ expect(load).toBeCalled();
+});