@astrojs/markdoc 0.0.4 → 0.0.5
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +19 -0
- package/components/TreeNode.ts +9 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +100 -10
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +9 -0
- package/package.json +6 -2
- package/src/index.ts +146 -11
- package/src/utils.ts +9 -0
- package/test/fixtures/image-assets/astro.config.mjs +10 -0
- package/test/fixtures/image-assets/node_modules/.bin/astro +17 -0
- package/test/fixtures/image-assets/package.json +9 -0
- package/test/fixtures/image-assets/src/assets/alias/cityscape.jpg +0 -0
- package/test/fixtures/image-assets/src/assets/relative/oar.jpg +0 -0
- package/test/fixtures/image-assets/src/content/docs/intro.mdoc +7 -0
- package/test/fixtures/image-assets/src/pages/index.astro +19 -0
- package/test/fixtures/image-assets/src/public/favicon.svg +9 -0
- package/test/image-assets.test.js +76 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[
|
|
2
|
-
[
|
|
3
|
-
[
|
|
4
|
-
[
|
|
5
|
-
[
|
|
1
|
+
[34m@astrojs/markdoc:build: [0mcache hit, replaying output [2m0cf4b43fcaef6989[0m
|
|
2
|
+
[34m@astrojs/markdoc:build: [0m
|
|
3
|
+
[34m@astrojs/markdoc:build: [0m> @astrojs/markdoc@0.0.5 build /home/runner/work/astro/astro/packages/integrations/markdoc
|
|
4
|
+
[34m@astrojs/markdoc:build: [0m> astro-scripts build "src/**/*.ts" && tsc
|
|
5
|
+
[34m@astrojs/markdoc:build: [0m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @astrojs/markdoc
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#6630](https://github.com/withastro/astro/pull/6630) [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Support automatic image optimization for Markdoc images when using `experimental.assets`. You can [follow our Assets guide](https://docs.astro.build/en/guides/assets/#enabling-assets-in-your-project) to enable this feature in your project. Then, start using relative or aliased image sources in your Markdoc files for automatic optimization:
|
|
8
|
+
|
|
9
|
+
```md
|
|
10
|
+
<!--Relative paths-->
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<!--Or configured aliases-->
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [[`b7194103e`](https://github.com/withastro/astro/commit/b7194103e39267bf59dcd6ba00f522e424219d16), [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638), [`45da39a86`](https://github.com/withastro/astro/commit/45da39a8642d64eb318840b18dfc2b5ccc6561bc), [`7daef9a29`](https://github.com/withastro/astro/commit/7daef9a2993b5d457f3d243a1ebfd1dd383b3327)]:
|
|
20
|
+
- astro@2.1.7
|
|
21
|
+
|
|
3
22
|
## 0.0.4
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/components/TreeNode.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AstroInstance } from 'astro';
|
|
2
2
|
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
|
3
3
|
import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js';
|
|
4
|
+
// @ts-expect-error Cannot find module 'astro:markdoc-assets' or its corresponding type declarations
|
|
5
|
+
import { Image } from 'astro:markdoc-assets';
|
|
4
6
|
import Markdoc from '@markdoc/markdoc';
|
|
5
7
|
import { MarkdocError, isCapitalized } from '../dist/utils.js';
|
|
6
8
|
|
|
@@ -45,10 +47,16 @@ export const ComponentNode = createComponent({
|
|
|
45
47
|
propagation: 'none',
|
|
46
48
|
});
|
|
47
49
|
|
|
50
|
+
const builtInComponents: Record<string, AstroInstance['default']> = {
|
|
51
|
+
Image,
|
|
52
|
+
};
|
|
53
|
+
|
|
48
54
|
export function createTreeNode(
|
|
49
55
|
node: RenderableTreeNode,
|
|
50
|
-
|
|
56
|
+
userComponents: Record<string, AstroInstance['default']> = {}
|
|
51
57
|
): TreeNode {
|
|
58
|
+
const components = { ...userComponents, ...builtInComponents };
|
|
59
|
+
|
|
52
60
|
if (typeof node === 'string' || typeof node === 'number') {
|
|
53
61
|
return { type: 'text', content: String(node) };
|
|
54
62
|
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Config } from '@markdoc/markdoc';
|
|
1
|
+
import type { Config as ReadonlyMarkdocConfig } from '@markdoc/markdoc';
|
|
2
2
|
import type { AstroIntegration } from 'astro';
|
|
3
|
-
export default function
|
|
3
|
+
export default function markdocIntegration(userMarkdocConfig?: ReadonlyMarkdocConfig): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import Markdoc from "@markdoc/markdoc";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
getAstroConfigPath,
|
|
6
|
+
isValidUrl,
|
|
7
|
+
MarkdocError,
|
|
8
|
+
parseFrontmatter,
|
|
9
|
+
prependForwardSlash
|
|
10
|
+
} from "./utils.js";
|
|
11
|
+
import { emitESMImage } from "astro/assets";
|
|
12
|
+
function markdocIntegration(userMarkdocConfig = {}) {
|
|
6
13
|
return {
|
|
7
14
|
name: "@astrojs/markdoc",
|
|
8
15
|
hooks: {
|
|
9
16
|
"astro:config:setup": async (params) => {
|
|
10
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
updateConfig,
|
|
19
|
+
config: astroConfig,
|
|
20
|
+
addContentEntryType
|
|
21
|
+
} = params;
|
|
22
|
+
updateConfig({
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })]
|
|
25
|
+
}
|
|
26
|
+
});
|
|
11
27
|
function getEntryInfo({ fileUrl, contents }) {
|
|
12
28
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
13
29
|
return {
|
|
@@ -20,16 +36,40 @@ function markdoc(markdocConfig = {}) {
|
|
|
20
36
|
addContentEntryType({
|
|
21
37
|
extensions: [".mdoc"],
|
|
22
38
|
getEntryInfo,
|
|
23
|
-
getRenderModule({ entry }) {
|
|
24
|
-
|
|
39
|
+
async getRenderModule({ entry }) {
|
|
40
|
+
var _a;
|
|
41
|
+
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
25
42
|
const ast = Markdoc.parse(entry.body);
|
|
26
|
-
const
|
|
27
|
-
|
|
43
|
+
const pluginContext = this;
|
|
44
|
+
const markdocConfig = {
|
|
45
|
+
...userMarkdocConfig,
|
|
28
46
|
variables: {
|
|
29
|
-
...
|
|
47
|
+
...userMarkdocConfig.variables,
|
|
30
48
|
entry
|
|
31
49
|
}
|
|
32
|
-
}
|
|
50
|
+
};
|
|
51
|
+
if ((_a = astroConfig.experimental) == null ? void 0 : _a.assets) {
|
|
52
|
+
await emitOptimizedImages(ast.children, {
|
|
53
|
+
astroConfig,
|
|
54
|
+
pluginContext,
|
|
55
|
+
filePath: entry._internal.filePath
|
|
56
|
+
});
|
|
57
|
+
markdocConfig.nodes ??= {};
|
|
58
|
+
markdocConfig.nodes.image = {
|
|
59
|
+
...Markdoc.nodes.image,
|
|
60
|
+
transform(node, config) {
|
|
61
|
+
const attributes = node.transformAttributes(config);
|
|
62
|
+
const children = node.transformChildren(config);
|
|
63
|
+
if (node.type === "image" && "__optimizedSrc" in node.attributes) {
|
|
64
|
+
const { __optimizedSrc, ...rest } = node.attributes;
|
|
65
|
+
return new Markdoc.Tag("Image", { ...rest, src: __optimizedSrc }, children);
|
|
66
|
+
} else {
|
|
67
|
+
return new Markdoc.Tag("img", attributes, children);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const content = Markdoc.transform(ast, markdocConfig);
|
|
33
73
|
return {
|
|
34
74
|
code: `import { jsx as h } from 'astro/jsx-runtime';
|
|
35
75
|
import { Renderer } from '@astrojs/markdoc/components';
|
|
@@ -49,6 +89,32 @@ Content[Symbol.for('astro.needsHeadRendering')] = true;`
|
|
|
49
89
|
}
|
|
50
90
|
};
|
|
51
91
|
}
|
|
92
|
+
async function emitOptimizedImages(nodeChildren, ctx) {
|
|
93
|
+
for (const node of nodeChildren) {
|
|
94
|
+
if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
|
|
95
|
+
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
|
|
96
|
+
if ((resolved == null ? void 0 : resolved.id) && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
|
|
97
|
+
const src = await emitESMImage(
|
|
98
|
+
resolved.id,
|
|
99
|
+
ctx.pluginContext.meta.watchMode,
|
|
100
|
+
ctx.pluginContext.emitFile,
|
|
101
|
+
{ config: ctx.astroConfig }
|
|
102
|
+
);
|
|
103
|
+
node.attributes.__optimizedSrc = src;
|
|
104
|
+
} else {
|
|
105
|
+
throw new MarkdocError({
|
|
106
|
+
message: `Could not resolve image ${JSON.stringify(
|
|
107
|
+
node.attributes.src
|
|
108
|
+
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await emitOptimizedImages(node.children, ctx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function shouldOptimizeImage(src) {
|
|
116
|
+
return !isValidUrl(src) && !src.startsWith("/");
|
|
117
|
+
}
|
|
52
118
|
function validateRenderProperties(markdocConfig, astroConfig) {
|
|
53
119
|
const tags = markdocConfig.tags ?? {};
|
|
54
120
|
const nodes = markdocConfig.nodes ?? {};
|
|
@@ -88,6 +154,30 @@ function validateRenderProperty({
|
|
|
88
154
|
function isCapitalized(str) {
|
|
89
155
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
90
156
|
}
|
|
157
|
+
function safeAssetsVirtualModulePlugin({
|
|
158
|
+
astroConfig
|
|
159
|
+
}) {
|
|
160
|
+
const virtualModuleId = "astro:markdoc-assets";
|
|
161
|
+
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
162
|
+
return {
|
|
163
|
+
name: "astro:markdoc-safe-assets-virtual-module",
|
|
164
|
+
resolveId(id) {
|
|
165
|
+
if (id === virtualModuleId) {
|
|
166
|
+
return resolvedVirtualModuleId;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
load(id) {
|
|
170
|
+
var _a;
|
|
171
|
+
if (id !== resolvedVirtualModuleId)
|
|
172
|
+
return;
|
|
173
|
+
if ((_a = astroConfig.experimental) == null ? void 0 : _a.assets) {
|
|
174
|
+
return `export { Image } from 'astro:assets';`;
|
|
175
|
+
} else {
|
|
176
|
+
return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
91
181
|
export {
|
|
92
|
-
|
|
182
|
+
markdocIntegration as default
|
|
93
183
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -47,4 +47,5 @@ export declare function getAstroConfigPath(fs: typeof fsMod, root: string): stri
|
|
|
47
47
|
export declare function prependForwardSlash(str: string): string;
|
|
48
48
|
export declare function validateComponentsProp(components: Record<string, AstroInstance['default']>): void;
|
|
49
49
|
export declare function isCapitalized(str: string): boolean;
|
|
50
|
+
export declare function isValidUrl(str: string): boolean;
|
|
50
51
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -84,10 +84,19 @@ const componentsPropValidator = z.record(
|
|
|
84
84
|
function isCapitalized(str) {
|
|
85
85
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
86
86
|
}
|
|
87
|
+
function isValidUrl(str) {
|
|
88
|
+
try {
|
|
89
|
+
new URL(str);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
export {
|
|
88
96
|
MarkdocError,
|
|
89
97
|
getAstroConfigPath,
|
|
90
98
|
isCapitalized,
|
|
99
|
+
isValidUrl,
|
|
91
100
|
parseFrontmatter,
|
|
92
101
|
prependForwardSlash,
|
|
93
102
|
validateComponentsProp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/markdoc",
|
|
3
3
|
"description": "Add support for Markdoc pages in your Astro site",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -28,16 +28,20 @@
|
|
|
28
28
|
"gray-matter": "^4.0.3",
|
|
29
29
|
"zod": "^3.17.3"
|
|
30
30
|
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"astro": "2.1.7"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
35
|
+
"astro": "2.1.7",
|
|
32
36
|
"@types/chai": "^4.3.1",
|
|
33
37
|
"@types/html-escaper": "^3.0.0",
|
|
34
38
|
"@types/mocha": "^9.1.1",
|
|
35
|
-
"astro": "2.1.5",
|
|
36
39
|
"astro-scripts": "0.0.14",
|
|
37
40
|
"chai": "^4.3.6",
|
|
38
41
|
"devalue": "^4.2.0",
|
|
39
42
|
"linkedom": "^0.14.12",
|
|
40
43
|
"mocha": "^9.2.2",
|
|
44
|
+
"rollup": "^3.20.1",
|
|
41
45
|
"vite": "^4.0.3"
|
|
42
46
|
},
|
|
43
47
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Config as ReadonlyMarkdocConfig,
|
|
3
|
+
ConfigType as MarkdocConfig,
|
|
4
|
+
Node,
|
|
5
|
+
} from '@markdoc/markdoc';
|
|
2
6
|
import Markdoc from '@markdoc/markdoc';
|
|
3
7
|
import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
|
4
8
|
import fs from 'node:fs';
|
|
5
9
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import
|
|
10
|
+
import type * as rollup from 'rollup';
|
|
11
|
+
import {
|
|
12
|
+
getAstroConfigPath,
|
|
13
|
+
isValidUrl,
|
|
14
|
+
MarkdocError,
|
|
15
|
+
parseFrontmatter,
|
|
16
|
+
prependForwardSlash,
|
|
17
|
+
} from './utils.js';
|
|
18
|
+
// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations.
|
|
19
|
+
import { emitESMImage } from 'astro/assets';
|
|
20
|
+
import type { Plugin as VitePlugin } from 'vite';
|
|
7
21
|
|
|
8
22
|
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
9
23
|
// `contentEntryType` is not a public API
|
|
@@ -11,12 +25,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
|
|
11
25
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
|
12
26
|
};
|
|
13
27
|
|
|
14
|
-
export default function
|
|
28
|
+
export default function markdocIntegration(
|
|
29
|
+
userMarkdocConfig: ReadonlyMarkdocConfig = {}
|
|
30
|
+
): AstroIntegration {
|
|
15
31
|
return {
|
|
16
32
|
name: '@astrojs/markdoc',
|
|
17
33
|
hooks: {
|
|
18
34
|
'astro:config:setup': async (params) => {
|
|
19
|
-
const {
|
|
35
|
+
const {
|
|
36
|
+
updateConfig,
|
|
37
|
+
config: astroConfig,
|
|
38
|
+
addContentEntryType,
|
|
39
|
+
} = params as SetupHookParams;
|
|
40
|
+
|
|
41
|
+
updateConfig({
|
|
42
|
+
vite: {
|
|
43
|
+
plugins: [safeAssetsVirtualModulePlugin({ astroConfig })],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
20
46
|
|
|
21
47
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
|
22
48
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
|
@@ -30,16 +56,44 @@ export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
|
|
|
30
56
|
addContentEntryType({
|
|
31
57
|
extensions: ['.mdoc'],
|
|
32
58
|
getEntryInfo,
|
|
33
|
-
getRenderModule({ entry }) {
|
|
34
|
-
validateRenderProperties(
|
|
59
|
+
async getRenderModule({ entry }) {
|
|
60
|
+
validateRenderProperties(userMarkdocConfig, astroConfig);
|
|
35
61
|
const ast = Markdoc.parse(entry.body);
|
|
36
|
-
const
|
|
37
|
-
|
|
62
|
+
const pluginContext = this;
|
|
63
|
+
const markdocConfig: MarkdocConfig = {
|
|
64
|
+
...userMarkdocConfig,
|
|
38
65
|
variables: {
|
|
39
|
-
...
|
|
66
|
+
...userMarkdocConfig.variables,
|
|
40
67
|
entry,
|
|
41
68
|
},
|
|
42
|
-
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (astroConfig.experimental?.assets) {
|
|
72
|
+
await emitOptimizedImages(ast.children, {
|
|
73
|
+
astroConfig,
|
|
74
|
+
pluginContext,
|
|
75
|
+
filePath: entry._internal.filePath,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
markdocConfig.nodes ??= {};
|
|
79
|
+
markdocConfig.nodes.image = {
|
|
80
|
+
...Markdoc.nodes.image,
|
|
81
|
+
transform(node, config) {
|
|
82
|
+
const attributes = node.transformAttributes(config);
|
|
83
|
+
const children = node.transformChildren(config);
|
|
84
|
+
|
|
85
|
+
if (node.type === 'image' && '__optimizedSrc' in node.attributes) {
|
|
86
|
+
const { __optimizedSrc, ...rest } = node.attributes;
|
|
87
|
+
return new Markdoc.Tag('Image', { ...rest, src: __optimizedSrc }, children);
|
|
88
|
+
} else {
|
|
89
|
+
return new Markdoc.Tag('img', attributes, children);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = Markdoc.transform(ast, markdocConfig);
|
|
96
|
+
|
|
43
97
|
return {
|
|
44
98
|
code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
|
45
99
|
content
|
|
@@ -56,7 +110,54 @@ export default function markdoc(markdocConfig: Config = {}): AstroIntegration {
|
|
|
56
110
|
};
|
|
57
111
|
}
|
|
58
112
|
|
|
59
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Emits optimized images, and appends the generated `src` to each AST node
|
|
115
|
+
* via the `__optimizedSrc` attribute.
|
|
116
|
+
*/
|
|
117
|
+
async function emitOptimizedImages(
|
|
118
|
+
nodeChildren: Node[],
|
|
119
|
+
ctx: {
|
|
120
|
+
pluginContext: rollup.PluginContext;
|
|
121
|
+
filePath: string;
|
|
122
|
+
astroConfig: AstroConfig;
|
|
123
|
+
}
|
|
124
|
+
) {
|
|
125
|
+
for (const node of nodeChildren) {
|
|
126
|
+
if (
|
|
127
|
+
node.type === 'image' &&
|
|
128
|
+
typeof node.attributes.src === 'string' &&
|
|
129
|
+
shouldOptimizeImage(node.attributes.src)
|
|
130
|
+
) {
|
|
131
|
+
// Attempt to resolve source with Vite.
|
|
132
|
+
// This handles relative paths and configured aliases
|
|
133
|
+
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
|
|
134
|
+
|
|
135
|
+
if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) {
|
|
136
|
+
const src = await emitESMImage(
|
|
137
|
+
resolved.id,
|
|
138
|
+
ctx.pluginContext.meta.watchMode,
|
|
139
|
+
ctx.pluginContext.emitFile,
|
|
140
|
+
{ config: ctx.astroConfig }
|
|
141
|
+
);
|
|
142
|
+
node.attributes.__optimizedSrc = src;
|
|
143
|
+
} else {
|
|
144
|
+
throw new MarkdocError({
|
|
145
|
+
message: `Could not resolve image ${JSON.stringify(
|
|
146
|
+
node.attributes.src
|
|
147
|
+
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
await emitOptimizedImages(node.children, ctx);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function shouldOptimizeImage(src: string) {
|
|
156
|
+
// Optimize anything that is NOT external or an absolute path to `public/`
|
|
157
|
+
return !isValidUrl(src) && !src.startsWith('/');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateRenderProperties(markdocConfig: ReadonlyMarkdocConfig, astroConfig: AstroConfig) {
|
|
60
161
|
const tags = markdocConfig.tags ?? {};
|
|
61
162
|
const nodes = markdocConfig.nodes ?? {};
|
|
62
163
|
|
|
@@ -105,3 +206,37 @@ function validateRenderProperty({
|
|
|
105
206
|
function isCapitalized(str: string) {
|
|
106
207
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
107
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* TODO: remove when `experimental.assets` is baselined.
|
|
212
|
+
*
|
|
213
|
+
* `astro:assets` will fail to resolve if the `experimental.assets` flag is not enabled.
|
|
214
|
+
* This ensures a fallback for the Markdoc renderer to safely import at the top level.
|
|
215
|
+
* @see ../components/TreeNode.ts
|
|
216
|
+
*/
|
|
217
|
+
function safeAssetsVirtualModulePlugin({
|
|
218
|
+
astroConfig,
|
|
219
|
+
}: {
|
|
220
|
+
astroConfig: Pick<AstroConfig, 'experimental'>;
|
|
221
|
+
}): VitePlugin {
|
|
222
|
+
const virtualModuleId = 'astro:markdoc-assets';
|
|
223
|
+
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
name: 'astro:markdoc-safe-assets-virtual-module',
|
|
227
|
+
resolveId(id) {
|
|
228
|
+
if (id === virtualModuleId) {
|
|
229
|
+
return resolvedVirtualModuleId;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
load(id) {
|
|
233
|
+
if (id !== resolvedVirtualModuleId) return;
|
|
234
|
+
|
|
235
|
+
if (astroConfig.experimental?.assets) {
|
|
236
|
+
return `export { Image } from 'astro:assets';`;
|
|
237
|
+
} else {
|
|
238
|
+
return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -145,3 +145,12 @@ const componentsPropValidator = z.record(
|
|
|
145
145
|
export function isCapitalized(str: string) {
|
|
146
146
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
export function isValidUrl(str: string): boolean {
|
|
150
|
+
try {
|
|
151
|
+
new URL(str);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
|
6
|
+
esac
|
|
7
|
+
|
|
8
|
+
if [ -z "$NODE_PATH" ]; then
|
|
9
|
+
export NODE_PATH="/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
10
|
+
else
|
|
11
|
+
export NODE_PATH="$NODE_PATH:/home/runner/work/astro/astro/node_modules/.pnpm/node_modules"
|
|
12
|
+
fi
|
|
13
|
+
if [ -x "$basedir/node" ]; then
|
|
14
|
+
exec "$basedir/node" "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
15
|
+
else
|
|
16
|
+
exec node "$basedir/../../../../../../../astro/astro.js" "$@"
|
|
17
|
+
fi
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getEntryBySlug } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
const intro = await getEntryBySlug('docs', 'intro');
|
|
5
|
+
const { Content } = await intro.render();
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
12
|
+
<meta name="viewport" content="width=device-width" />
|
|
13
|
+
<meta name="generator" content={Astro.generator} />
|
|
14
|
+
<title>Astro</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<Content />
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
2
|
+
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
3
|
+
<style>
|
|
4
|
+
path { fill: #000; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
path { fill: #FFF; }
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseHTML } from 'linkedom';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { loadFixture } from '../../../astro/test/test-utils.js';
|
|
4
|
+
|
|
5
|
+
const root = new URL('./fixtures/image-assets/', import.meta.url);
|
|
6
|
+
|
|
7
|
+
describe('Markdoc - Image assets', () => {
|
|
8
|
+
let baseFixture;
|
|
9
|
+
|
|
10
|
+
before(async () => {
|
|
11
|
+
baseFixture = await loadFixture({
|
|
12
|
+
root,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('dev', () => {
|
|
17
|
+
let devServer;
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
devServer = await baseFixture.startDevServer();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(async () => {
|
|
24
|
+
await devServer.stop();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses public/ image paths unchanged', async () => {
|
|
28
|
+
const res = await baseFixture.fetch('/');
|
|
29
|
+
const html = await res.text();
|
|
30
|
+
const { document } = parseHTML(html);
|
|
31
|
+
expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('transforms relative image paths to optimized path', async () => {
|
|
35
|
+
const res = await baseFixture.fetch('/');
|
|
36
|
+
const html = await res.text();
|
|
37
|
+
const { document } = parseHTML(html);
|
|
38
|
+
expect(document.querySelector('#relative > img')?.src).to.equal(
|
|
39
|
+
'/_image?href=%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&f=webp'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('transforms aliased image paths to optimized path', async () => {
|
|
44
|
+
const res = await baseFixture.fetch('/');
|
|
45
|
+
const html = await res.text();
|
|
46
|
+
const { document } = parseHTML(html);
|
|
47
|
+
expect(document.querySelector('#alias > img')?.src).to.equal(
|
|
48
|
+
'/_image?href=%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp'
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('build', () => {
|
|
54
|
+
before(async () => {
|
|
55
|
+
await baseFixture.build();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('uses public/ image paths unchanged', async () => {
|
|
59
|
+
const html = await baseFixture.readFile('/index.html');
|
|
60
|
+
const { document } = parseHTML(html);
|
|
61
|
+
expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('transforms relative image paths to optimized path', async () => {
|
|
65
|
+
const html = await baseFixture.readFile('/index.html');
|
|
66
|
+
const { document } = parseHTML(html);
|
|
67
|
+
expect(document.querySelector('#relative > img')?.src).to.match(/^\/_astro\/oar.*\.webp$/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('transforms aliased image paths to optimized path', async () => {
|
|
71
|
+
const html = await baseFixture.readFile('/index.html');
|
|
72
|
+
const { document } = parseHTML(html);
|
|
73
|
+
expect(document.querySelector('#alias > img')?.src).to.match(/^\/_astro\/cityscape.*\.webp$/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|