@astrojs/markdoc (#6209)
* chore: integration setup * feat: get markdoc contents * wip: expose Markdoc from integration * feat: basic Astro renderer in with-markdoc * fix: component render bug * deps: stringify-attributes * fix: render attributes to html * wip: play with separate markdoc config * wip: get dream API for file loader working * chore: unit tests * deps: graymatter * feat: addContentEntryType integration hook * deps: move to @astrojs/markdoc * feat: move Renderer to markdoc, get Content component! * wip: scaffold content types * deps: mdx * wip: move mdx to collection type API * refactor: move plain md to content entry type * refactor: pass file contents * oops, forgot to commit untracked * fix: markdoc.config loading * refactor: remove fallback loader * chore: remove unused dream file * deps: devalue, test fixture * test: entry and collections parse * play: make sure md also works * deps: add shiki for Code comp error * fix: remove "components" from tsconfig * chore: ignore `.astro` type error * fix: avoid import if no config present (prod build error) * fix: stop bundling markdoc for isTag * test: prod builds * test: content component dev and build * chore: Markdoc working! log * fix: ContentEntryType import * chore: remove content-types. Too early! * chore: remove unused options object * play: add docs example with Aside and Since ported * chore: with-markdoc strictNullChecks * chore: unused file * feat: allow Render type injection * feat: content prop types for markdoc! * chore: clean up Markdoc starter to essentials * chore: unused style tag * chore: remove unused deps * fix: glob for single content extension * chore: remove unused fixture dep * chore: remove markdoc.config loader * docs: update example README * docs: @astrojs/markdoc README intro * fix: line endings in test * docs: add usage and examples to Markdoc README * docs: change with-markdoc title * docs: README edits * refactor: clean up astroNode * nit: reorder type import * docs: add note on Aside src * chore: changeset * nit: 0.0.0 to avoid version bump * fix: lock * refactor: remove unneeded async * fix: import types from @astrojs/markdoc * fix: type inferencing in preview * fix: remove unneeded html-escaper * deps: remove html-escaper * feat: support `.md` overrides for content collections * chore: mdoc -> md * feat: generate `.md` types override * Revert "feat: support `.md` overrides for content collections" This reverts commitc06f83ef85
. * Squashed commit of `markdoc-poc` commit bd0dd9d71938da22f596ddd1662d9b91707f80c7 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 15 09:43:04 2023 -0500 Revert "feat: support `.md` overrides for content collections" This reverts commitc06f83ef85
. commit 5b9a1f33f4d0ffd4a63a81a86652fee032b75d76 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 13:48:56 2023 -0500 feat: generate `.md` types override commit e464ae894ed69cf492de1d3e8018d9a17c00d7a3 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:56:11 2023 -0500 chore: mdoc -> md commit b74fd6a7d6b973b2bfd9b2ab9567801ff4d7fec1 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:56:01 2023 -0500 feat: support `.md` overrides for content collections commit bdcc18a7c03e3524927ba7c1b18ad2fd80a2e994 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:30:48 2023 -0500 deps: remove html-escaper commit f44b57f0fa2ac923a3f2a46471fda710e92d5824 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:30:42 2023 -0500 fix: remove unneeded html-escaper commit fb7919498aca6beded444ed5922f3598d244d30c Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:08:04 2023 -0500 fix: type inferencing in preview commit 0c05034b321a82f2f054b527538d57f45f096822 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 11:32:19 2023 -0500 fix: import types from @astrojs/markdoc commit 57e741a6306b801ac4ce47f20f7664518359611d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 11:28:33 2023 -0500 refactor: remove unneeded async commit b19d85451e60da281bc32a2a180452987a049479 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 10:26:20 2023 -0500 fix: lock commit b1083a14f9fefe5e7a9f2a8c7c31626a7430c851 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 10:19:50 2023 -0500 nit: 0.0.0 to avoid version bump commit 71891ebe9dd54b495e965bf064ee78aaca9dfede Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:33:27 2023 -0500 chore: changeset commit 444752a5e27c4f5bda54ac024eca9a7ab1c4563f Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:30:10 2023 -0500 docs: add note on Aside src commit 619d4b3fdf02cc34ea3f8f9a0b4d93db273bb0a1 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:25:56 2023 -0500 nit: reorder type import commit e914da05706382b17e1b8f3404fc2852002a5dc7 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:25:38 2023 -0500 refactor: clean up astroNode commit 2dfc226f56dc6a9e3e3ace72d4c3889a1fde4e6d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:08:56 2023 -0500 docs: README edits commit 3c7b85c7d9e3c75d551baf33aba9a646d0d61cba Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:07:29 2023 -0500 docs: change with-markdoc title commit 29cdea9a0be439b631389cbc906751185489cbe9 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:29:10 2023 -0500 docs: add usage and examples to Markdoc README commit e397a80179e9bd934ef8875155d7491b1d4e077a Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:18:54 2023 -0500 fix: line endings in test commit f9ebf59982fa7dc1202368da344897f54de7f259 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:09:15 2023 -0500 docs: @astrojs/markdoc README intro commit 7800e9c9dac946f89e139b200a0083b53942118d Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:08:07 2023 -0500 docs: update example README commit f0b900a6fa7f0775dfab7600c4fb04c9b64e7439 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:04:34 2023 -0500 chore: remove markdoc.config loader commit 3f040e44d0c639dc51976abe75f46c57f0be60fc Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 17:58:52 2023 -0500 chore: remove unused fixture dep commit b2ea6c26df6acf1df8f06d2feb9c91da8c8eafae Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 17:58:25 2023 -0500 fix: glob for single content extension commit f4e171c05c421f8696ebc406c9d4d28967f80690 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:10:39 2023 -0500 chore: remove unused deps commit d9095d0284b0fc22d5da367771bc5286eb8f1926 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:10:31 2023 -0500 chore: unused style tag commit a5435f13575577bf6a954c68eef9e883959568dc Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:08:52 2023 -0500 chore: clean up Markdoc starter to essentials commit 815e2f2bad5617ab077d278d7c751f34c22dd0d9 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:13:02 2023 -0500 feat: content prop types for markdoc! commit 8bcd23c4e70b4900b16ebf5a30dad6ec998f4092 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:05:40 2023 -0500 feat: allow Render type injection commit cdb7e9e4878888c0883d4082d2415f4d85050389 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:04:37 2023 -0500 chore: unused file commit 0df33ea37d4ca9bd933be54d7f8b455fce174679 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:04:31 2023 -0500 chore: with-markdoc strictNullChecks commit 377c6d766bd51469f09e1f946a1c2b77affe6620 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 17:39:35 2023 -0500 play: add docs example with Aside and Since ported commit f9f811257167da7a5e05a7de3a12b58e7b3568b1 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:22:47 2023 -0500 chore: remove unused options object commit c9a66bfeb0ce55ef0a1c0c6ed2da746d5250c795 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:18:27 2023 -0500 chore: remove content-types. Too early! commit 67f49e86a980c5d858ad0e384bdc67fad5e096e6 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:18:20 2023 -0500 fix: ContentEntryType import commit 9b93d8189213a12346520fa8a71eae824386ad3e Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:10:06 2023 -0500 chore: Markdoc working! log commit 90f77645ef3985fbba1badeb4413759626290b1a Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:09:58 2023 -0500 test: content component dev and build commit 14559f6a40af63106cebeba210e73392d5d1c37d Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:01:10 2023 -0500 test: prod builds commit a0dcbff73cf8b3cf9b55434a8fb2b0b683903c5f Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:01:04 2023 -0500 fix: stop bundling markdoc for isTag commit cb1904876f074c50c206131e9588878230bbab47 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:53 2023 -0500 fix: avoid import if no config present (prod build error) commit bda8295e7920638c013875ab9fe31ce87743c95f Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:33 2023 -0500 chore: ignore `.astro` type error commit 28a6d7993a227c68aeca747661254ff68883244a Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:12 2023 -0500 fix: remove "components" from tsconfig commit 0297bb749599aa8063e685a7ece738afe3ae77cc Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:01 2023 -0500 deps: add shiki for Code comp error commit e4f141e558b3d76b761bec69e694eaa40cdf250d Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 08:45:37 2023 -0500 play: make sure md also works commit dfdb35f0888ee83457ee825172f7d8485b7d2a43 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:25:18 2023 -0500 test: entry and collections parse commit eaf4994bf309955209f406d7db0863775fdf4fe0 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:25:07 2023 -0500 deps: devalue, test fixture commit 3f71932c04f8064cfe74b7f4b02409c681925e4a Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:24:47 2023 -0500 chore: remove unused dream file commit b182686f88878cdf003efba798edb69372872b21 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:29:32 2023 -0500 refactor: remove fallback loader commit 0540d701ccb33db7a82be861c36dcafec0dbbb81 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:26:21 2023 -0500 fix: markdoc.config loading commit 575dadf70e7b5aa263e30c8b496a1cceebd31bb4 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:16:18 2023 -0500 oops, forgot to commit untracked commit 9746a97bc2dfcf63bfd685f541ffbd229ea134ca Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:16:02 2023 -0500 refactor: pass file contents commit 534a658325cbddbb153bea1c1338d13011eb0e4f Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:13:38 2023 -0500 refactor: move plain md to content entry type commit 24df79b1475c12ccefaadb2392e9d5911c491872 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 15:51:44 2023 -0500 wip: move mdx to collection type API commit cc44e4fa3453353041fdbe9424d96040f93f28a7 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 15:50:52 2023 -0500 deps: mdx commit c30c1b3dcf6da3a075c1f07260ae893ef93e07e2 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 10:10:39 2023 -0500 wip: scaffold content types commit 2487aedf9deb74a5725ba8393766b68f60b74af8 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 09:53:35 2023 -0500 feat: move Renderer to markdoc, get Content component! commit 44faff911b8de6f6da45f195dad0a56a426b076e Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 09:53:06 2023 -0500 deps: move to @astrojs/markdoc commit bef26906f3f2f1104b88332778ee219cf062fe88 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 16:01:34 2023 -0500 feat: addContentEntryType integration hook commit 508568f5117a56fc4ef14db9d0482cdfcfe5007d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 16:00:42 2023 -0500 deps: graymatter commit cb706020391b4b61f3854b3e9a9d9190a94785c9 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 14:00:27 2023 -0500 chore: unit tests commit 545246d15362b8f981f8c315d3b7d0341626eeb8 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 14:00:23 2023 -0500 wip: get dream API for file loader working commit 97664e0f862f7f6e0c9ce8478b3bed58432f6426 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 18:09:23 2023 -0500 wip: play with separate markdoc config commit c5d8a3336cc9f5dd7b4115b7f86fdb86474ca462 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 17:04:22 2023 -0500 fix: render attributes to html commit 82e30a0083b20c85307726027ee7e74df7378632 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 17:04:14 2023 -0500 deps: stringify-attributes commit b72ba6167db88d5042eee51d5912d4270417bca5 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:33:27 2023 -0500 fix: component render bug commit 59f1e5c6f89d2c7457a0b0e0acb838bf945d39bf Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:11:15 2023 -0500 feat: basic Astro renderer in with-markdoc commit de15a7213d9afbc94dd0e50f713c9a02dc8e90e5 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:11:01 2023 -0500 wip: expose Markdoc from integration commit 83bfe4441b652a7eb880486785dc2c5eda08e3b3 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 11:21:17 2023 -0500 feat: get markdoc contents commit 6f500689a00b8f8c2151254930fda24e0b3e4207 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 11:13:57 2023 -0500 chore: integration setup * Squashed commit of `markdoc-poc` commit bd0dd9d71938da22f596ddd1662d9b91707f80c7 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 15 09:43:04 2023 -0500 Revert "feat: support `.md` overrides for content collections" This reverts commitc06f83ef85
. commit 5b9a1f33f4d0ffd4a63a81a86652fee032b75d76 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 13:48:56 2023 -0500 feat: generate `.md` types override commit e464ae894ed69cf492de1d3e8018d9a17c00d7a3 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:56:11 2023 -0500 chore: mdoc -> md commit b74fd6a7d6b973b2bfd9b2ab9567801ff4d7fec1 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:56:01 2023 -0500 feat: support `.md` overrides for content collections commit bdcc18a7c03e3524927ba7c1b18ad2fd80a2e994 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:30:48 2023 -0500 deps: remove html-escaper commit f44b57f0fa2ac923a3f2a46471fda710e92d5824 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:30:42 2023 -0500 fix: remove unneeded html-escaper commit fb7919498aca6beded444ed5922f3598d244d30c Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 12:08:04 2023 -0500 fix: type inferencing in preview commit 0c05034b321a82f2f054b527538d57f45f096822 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 11:32:19 2023 -0500 fix: import types from @astrojs/markdoc commit 57e741a6306b801ac4ce47f20f7664518359611d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 11:28:33 2023 -0500 refactor: remove unneeded async commit b19d85451e60da281bc32a2a180452987a049479 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 10:26:20 2023 -0500 fix: lock commit b1083a14f9fefe5e7a9f2a8c7c31626a7430c851 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 10:19:50 2023 -0500 nit: 0.0.0 to avoid version bump commit 71891ebe9dd54b495e965bf064ee78aaca9dfede Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:33:27 2023 -0500 chore: changeset commit 444752a5e27c4f5bda54ac024eca9a7ab1c4563f Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:30:10 2023 -0500 docs: add note on Aside src commit 619d4b3fdf02cc34ea3f8f9a0b4d93db273bb0a1 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:25:56 2023 -0500 nit: reorder type import commit e914da05706382b17e1b8f3404fc2852002a5dc7 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:25:38 2023 -0500 refactor: clean up astroNode commit 2dfc226f56dc6a9e3e3ace72d4c3889a1fde4e6d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:08:56 2023 -0500 docs: README edits commit 3c7b85c7d9e3c75d551baf33aba9a646d0d61cba Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 14 09:07:29 2023 -0500 docs: change with-markdoc title commit 29cdea9a0be439b631389cbc906751185489cbe9 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:29:10 2023 -0500 docs: add usage and examples to Markdoc README commit e397a80179e9bd934ef8875155d7491b1d4e077a Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:18:54 2023 -0500 fix: line endings in test commit f9ebf59982fa7dc1202368da344897f54de7f259 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:09:15 2023 -0500 docs: @astrojs/markdoc README intro commit 7800e9c9dac946f89e139b200a0083b53942118d Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:08:07 2023 -0500 docs: update example README commit f0b900a6fa7f0775dfab7600c4fb04c9b64e7439 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 18:04:34 2023 -0500 chore: remove markdoc.config loader commit 3f040e44d0c639dc51976abe75f46c57f0be60fc Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 17:58:52 2023 -0500 chore: remove unused fixture dep commit b2ea6c26df6acf1df8f06d2feb9c91da8c8eafae Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 17:58:25 2023 -0500 fix: glob for single content extension commit f4e171c05c421f8696ebc406c9d4d28967f80690 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:10:39 2023 -0500 chore: remove unused deps commit d9095d0284b0fc22d5da367771bc5286eb8f1926 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:10:31 2023 -0500 chore: unused style tag commit a5435f13575577bf6a954c68eef9e883959568dc Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 15:08:52 2023 -0500 chore: clean up Markdoc starter to essentials commit 815e2f2bad5617ab077d278d7c751f34c22dd0d9 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:13:02 2023 -0500 feat: content prop types for markdoc! commit 8bcd23c4e70b4900b16ebf5a30dad6ec998f4092 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:05:40 2023 -0500 feat: allow Render type injection commit cdb7e9e4878888c0883d4082d2415f4d85050389 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:04:37 2023 -0500 chore: unused file commit 0df33ea37d4ca9bd933be54d7f8b455fce174679 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 13 14:04:31 2023 -0500 chore: with-markdoc strictNullChecks commit 377c6d766bd51469f09e1f946a1c2b77affe6620 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 17:39:35 2023 -0500 play: add docs example with Aside and Since ported commit f9f811257167da7a5e05a7de3a12b58e7b3568b1 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:22:47 2023 -0500 chore: remove unused options object commit c9a66bfeb0ce55ef0a1c0c6ed2da746d5250c795 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:18:27 2023 -0500 chore: remove content-types. Too early! commit 67f49e86a980c5d858ad0e384bdc67fad5e096e6 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:18:20 2023 -0500 fix: ContentEntryType import commit 9b93d8189213a12346520fa8a71eae824386ad3e Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:10:06 2023 -0500 chore: Markdoc working! log commit 90f77645ef3985fbba1badeb4413759626290b1a Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:09:58 2023 -0500 test: content component dev and build commit 14559f6a40af63106cebeba210e73392d5d1c37d Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:01:10 2023 -0500 test: prod builds commit a0dcbff73cf8b3cf9b55434a8fb2b0b683903c5f Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:01:04 2023 -0500 fix: stop bundling markdoc for isTag commit cb1904876f074c50c206131e9588878230bbab47 Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:53 2023 -0500 fix: avoid import if no config present (prod build error) commit bda8295e7920638c013875ab9fe31ce87743c95f Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:33 2023 -0500 chore: ignore `.astro` type error commit 28a6d7993a227c68aeca747661254ff68883244a Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:12 2023 -0500 fix: remove "components" from tsconfig commit 0297bb749599aa8063e685a7ece738afe3ae77cc Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 09:00:01 2023 -0500 deps: add shiki for Code comp error commit e4f141e558b3d76b761bec69e694eaa40cdf250d Author: bholmesdev <hey@bholmes.dev> Date: Fri Feb 10 08:45:37 2023 -0500 play: make sure md also works commit dfdb35f0888ee83457ee825172f7d8485b7d2a43 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:25:18 2023 -0500 test: entry and collections parse commit eaf4994bf309955209f406d7db0863775fdf4fe0 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:25:07 2023 -0500 deps: devalue, test fixture commit 3f71932c04f8064cfe74b7f4b02409c681925e4a Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 13:24:47 2023 -0500 chore: remove unused dream file commit b182686f88878cdf003efba798edb69372872b21 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:29:32 2023 -0500 refactor: remove fallback loader commit 0540d701ccb33db7a82be861c36dcafec0dbbb81 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:26:21 2023 -0500 fix: markdoc.config loading commit 575dadf70e7b5aa263e30c8b496a1cceebd31bb4 Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:16:18 2023 -0500 oops, forgot to commit untracked commit 9746a97bc2dfcf63bfd685f541ffbd229ea134ca Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:16:02 2023 -0500 refactor: pass file contents commit 534a658325cbddbb153bea1c1338d13011eb0e4f Author: bholmesdev <hey@bholmes.dev> Date: Thu Feb 9 12:13:38 2023 -0500 refactor: move plain md to content entry type commit 24df79b1475c12ccefaadb2392e9d5911c491872 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 15:51:44 2023 -0500 wip: move mdx to collection type API commit cc44e4fa3453353041fdbe9424d96040f93f28a7 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 15:50:52 2023 -0500 deps: mdx commit c30c1b3dcf6da3a075c1f07260ae893ef93e07e2 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 10:10:39 2023 -0500 wip: scaffold content types commit 2487aedf9deb74a5725ba8393766b68f60b74af8 Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 09:53:35 2023 -0500 feat: move Renderer to markdoc, get Content component! commit 44faff911b8de6f6da45f195dad0a56a426b076e Author: bholmesdev <hey@bholmes.dev> Date: Wed Feb 8 09:53:06 2023 -0500 deps: move to @astrojs/markdoc commit bef26906f3f2f1104b88332778ee219cf062fe88 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 16:01:34 2023 -0500 feat: addContentEntryType integration hook commit 508568f5117a56fc4ef14db9d0482cdfcfe5007d Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 16:00:42 2023 -0500 deps: graymatter commit cb706020391b4b61f3854b3e9a9d9190a94785c9 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 14:00:27 2023 -0500 chore: unit tests commit 545246d15362b8f981f8c315d3b7d0341626eeb8 Author: bholmesdev <hey@bholmes.dev> Date: Tue Feb 7 14:00:23 2023 -0500 wip: get dream API for file loader working commit 97664e0f862f7f6e0c9ce8478b3bed58432f6426 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 18:09:23 2023 -0500 wip: play with separate markdoc config commit c5d8a3336cc9f5dd7b4115b7f86fdb86474ca462 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 17:04:22 2023 -0500 fix: render attributes to html commit 82e30a0083b20c85307726027ee7e74df7378632 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 17:04:14 2023 -0500 deps: stringify-attributes commit b72ba6167db88d5042eee51d5912d4270417bca5 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:33:27 2023 -0500 fix: component render bug commit 59f1e5c6f89d2c7457a0b0e0acb838bf945d39bf Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:11:15 2023 -0500 feat: basic Astro renderer in with-markdoc commit de15a7213d9afbc94dd0e50f713c9a02dc8e90e5 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 16:11:01 2023 -0500 wip: expose Markdoc from integration commit 83bfe4441b652a7eb880486785dc2c5eda08e3b3 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 11:21:17 2023 -0500 feat: get markdoc contents commit 6f500689a00b8f8c2151254930fda24e0b3e4207 Author: bholmesdev <hey@bholmes.dev> Date: Mon Feb 6 11:13:57 2023 -0500 chore: integration setup * feat: add `--perf` flag to skip md plugins * wip: performance pkg with md fixture * refactor: move rendering to util * refactor: generate posts instead of commiting * deps: npm-run-all * chore: mdoc and mdx fixtures * chore: remove generated files from git * chore: lockfile * chore: remove .astro types from remote * refactor: --perf -> --internal-ci-perf * chore: log ext and directory on write * refactor: --perf -> env var * feat: add perf benchmark check to mdx * refactor: isPerfBenchmark * chore: refine generated ignore * feat: test simple and with-components cases * chore: remove old --internal-ci reference * refactor: ASTRO_CI_PERFORMANCE_RUN -> ASTRO_PERFORMANCE_BENCHMARK * chore: remove accidental file * feat: add react-component render benchmark * chore: add `--format` flag to benchmark * refactor: move markdoc transform to build time * docs: only used in content collections Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * chore: use Markdoc terms for transform * refactor: remove Markdoc export * refactor: remove advanced component API * test: update fixture to match new config * chore: with-markdoc filetree * docs: add "exclusively" to README * docs: query for -> query your collection * docs: update config to new Markdoc options * docs: astro-ui-frameworks -> astro-components * edit: add client component instructions * nit: add "today" to imply future support * docs: add separate HTML and component examples * fix: inconsistent tabs v spaces * fix: indentation * deps: zod * chore: check components are capitalized names * chore: raise error if no matching component found * chore: hand `mjs` highlighting in error overlay * chore: validate tags and nodes * refactor: use MarkdocError on render errors * fix: handle possible cache miss * chore: use error hint * test: update Markdoc node config test * chore: clarify Markdoc config * docs: reshuffle note * chore: update to `nodes` rec * docs: clarify Markdoc attributes * docs: add editor integration * chore: update example coding challenge * chore: update DocsContent comments * fix: cant find zod for some reason * fix: potentially unreliable cache * chore: remove revealSecret from ex * wip: forward slash?? * fix: forward slash. * chore: lint * chore: ignore statements * chore: clarify semi-private hooks * docs: with-mdx -> with-markdoc Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * nit: don't capitalize cc Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs: ::: Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * nit: remove IFFE * chore: remove accidental changelog * nit: attempt pathToFileURL * nit: tested if * fix: smartypants should be avoided in benchmark! * nit: remove `any` * nit: extract component zod obj * fix: move validateComponents to render comp * nit: remove unused return * nit: move shiki highlighter consts to outer scope * nit: remove `any` from mdx * refactor: add type defs for private integration hooks * fix: type error in mdx defaults * fix: broke extendMarkdownConfig, oops! * Revert "nit: attempt pathToFileURL" This reverts commit45fac9922d
. * fix: only validate when components is defined * fix: remove types from git * fix: migrate mdoc perf benchmark to new config * chore: delete more sneaky types * docs: add usage to changeset * docs: update config example in README * docs: add manual rendering instructions * Update examples/with-markdoc/README.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * docs: reorder configuration options * docs: "see the next section" * chore: add numPosts as a benchmark flag * chore: bump to minor changesets for MDX * chore: update TODO docs links * docs: update component key naming and node instr * docs: redraft markdoc runtime config intro * Squashed commit of the following: commitfbab73c96e
Author: matthewp <matthewp@users.noreply.github.com> Date: Tue Mar 7 16:38:11 2023 +0000 [ci] format commita206106098
Author: Matthew Phillips <matthew@skypack.dev> Date: Tue Mar 7 11:35:54 2023 -0500 Expose the ssr manifest (#6435) * Expose the ssr manifest * Add changeset * Add types for virtual mod commit2751584387
Author: Princesseuh <Princesseuh@users.noreply.github.com> Date: Tue Mar 7 15:14:15 2023 +0000 [ci] format commit694918a56b
Author: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Tue Mar 7 16:12:21 2023 +0100 Implement RFC "A core story for images" (#6344) * feat(assets): Add Vite plugin * feat(images): Set up Image component * fix(types): Attempt to fix type generation * Revert "fix(types): Attempt to fix type generation" This reverts commit063aa276e2
. * fix(image): Fix image types causing build to fail * feat(image): Implement client side part * feat(services): Allow arbitrary transforms parameters * fix(image): Fix paths and types * config(types): Update config types to provide completions for available services * feat(image): Add serving in dev * feat(image): Improve type error messages * refactor(image): Move sharp's parseParams to baseService * refactor(image): Skip work in dev for remote servies * feat(image): Add support for remote images * feat(image): Add squoosh service * chore: update export map * refactor(image): Abstract attributes handling by services * config(vercel): Remove test image service * feat(image): Support for relative images in Markdown (WIP) * feat(images): Add support for relative images in Markdown * feat(image): Update with RFC feedback * fix(image): Fix alt error on getImage * feat(image): Add support for assets validation through content collections * feat(image): Remove validateTransform * feat(image): Move to assets folder * fix(image): Fix package exports * feat(image): Add static imports references to virtual moduel * fix(image): Fix images from content collections not working when embedded * chore: lockfile * fix(markdown): Fix type * fix(images): Flag enhanced images behing an experimental flag * config(example): Update images example conifg * fix(image): Fix types * fix(image): Fix asset type for strict, allow arbritary input and output formats * chore: fix example check * feat(image): Emit assets for ESM imported images * Add initial core image tests (#6381) * feat(images): Make frontmatter extraction more generic than images for future * feat(image): Add support for building * fix(image): Fix types * fix(images): Fix compatibility with image integration * feat(images): Cuter generation stats * fix(images): Globals are unsafe, it turns out * fix(images): Only generate images if flag is enabled * fix(images): Only create `addStaticImage` in build * feat(images): Add SSR endpoint * fix(images): Only inject route in SSR * Add tests for SSR * Remove console.log * Updated lockfile * rename to satisfy the link gods * skip build tests for now * fix(images): Fix WASM files not being copied in dev * feat(images): Add quality presets * fix build tests running * Remove console.log * Add tests for getImage * Test local services * Test the content collections API * Add tests for quality * Skipping content collections test * feat(image): Add support for `~/assets` alias * test(image): Add tests for aliases in dev * Fix windows + content collections * test(image): Add tests for aliased images and images in Markdown * Fix markdown images being built * Should be posix join * Use the optimized image * fix test * Fixes windows smoke * fix(image): Nits * feat(images): Add automatic update for `env.d.ts` when experimental images are enabled * fix(images): Revert env.d.ts change if the user opted-out of the experimental image support * chore: remove bad image example project * feat(image): Rename `experimental.images` to `experimental.assets` * fix(images): Remove unused code in MDX integration * chore: Remove unrelated change * fix(images): Remove export from astro/components * Fix, esm import on Win * test(images): Add test for format * fix(images): Add `client-image.d.ts` to export map * chore: changeset * fix(images): Adjust with feedback, no more automatic refine, asset() -> image() * fix(images): Fix types * fix(images): Remove unnecessary spread * fix(images): Better types for parseUrl and transform * fix(images): Fix types * fix(images): Adjust from feedback * fix(images): Pass width and height through getHTMLAttributes even if they're not added by the uesr * fix(images): Recusirsively extract frontmatter assets * fix(images): Use a reduce instead * feat(images): Add support for data: URIs * chore: changeset * docs(images): Misc docs fixes * Update .changeset/gold-rocks-cry.md Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update .changeset/gold-rocks-cry.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/types.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/types.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> --------- Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: Matthew Phillips <matthew@matthewphillips.info> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> commit377530a810
Author: ematipico <ematipico@users.noreply.github.com> Date: Tue Mar 7 13:43:21 2023 +0000 [ci] format commitfe88f89512
Author: Emanuele Stoppa <my.burning@gmail.com> Date: Tue Mar 7 13:41:24 2023 +0000 chore: use directive `@ts-expect-error` instead of `@ts-ignore` (#6429) commite1858e6334
Author: ematipico <ematipico@users.noreply.github.com> Date: Tue Mar 7 06:57:52 2023 +0000 [ci] format commit75921b3cd9
Author: Emanuele Stoppa <my.burning@gmail.com> Date: Tue Mar 7 06:55:41 2023 +0000 feat(cli): add `--watch` to `astro check` command (#6356) * feat(cli): add `--watch` to `astro check` command * chore: refactor in a leaner way, logic not changed * chore: lint * chore: revert changes in sync command * chore: tweak server settings * test: add one test case * chore: increase timeout * test: predictable testing * chore: add changeset * chore: code suggestions * code suggestions * chore: use directly `chokidar` * chore: tweak code * fix: open documents first * chore: disable test * chore: code suggestions * chore: code suggestions * Apply suggestions from code review Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> * code suggestions * chore: rebase --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> commit1291afc09d
Author: Bjorn Lu <bjornlu.dev@gmail.com> Date: Tue Mar 7 14:50:34 2023 +0800 Fix changeset run (#6442) commit00a0af7ed4
Author: Bjorn Lu <bjornlu.dev@gmail.com> Date: Tue Mar 7 10:52:47 2023 +0800 Move benchmark package and update changeset config (#6433) commitaf05a4fa46
Author: Nate Moore <natemoo-re@users.noreply.github.com> Date: Mon Mar 6 14:06:33 2023 -0600 Update README.md (#6437) * Update README.md * Update README.md commit8ebf4b7481
Author: Nate Moore <natemoo-re@users.noreply.github.com> Date: Mon Mar 6 14:03:33 2023 -0600 chore: update branding assets (#6436) commitafbbc4d5bf
Author: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Mon Mar 6 19:57:16 2023 +0100 Update compilation target for Node 16 (#6213) * config(esbuild): Update esbuild target to node16 * config(package): Update root package.json node engine * config(tsconfig): Update all the tsconfigs module and targets * chore: changeset * chore: remove unneeded file commit18acae3edc
Author: Sarah Rainsberger <sarah@rainsberger.ca> Date: Mon Mar 6 14:36:40 2023 -0400 [error docs] update link for client-side scripts (#6423) commit8b49d1781d
Author: ematipico <ematipico@users.noreply.github.com> Date: Mon Mar 6 17:03:18 2023 +0000 [ci] format commita4a74ab70c
Author: Emanuele Stoppa <my.burning@gmail.com> Date: Mon Mar 6 16:58:56 2023 +0000 feat(cli): add help flags to various commands (#6394) Co-authored-by: Happydev <81974850+MoustaphaDev@users.noreply.github.com> commitb087b83fe2
Author: Dennis Morello <dennismorello@gmail.com> Date: Mon Mar 6 17:38:42 2023 +0100 Add getStaticPaths type helpers to infer params and props (#6150) * feat(astro): add InferGetStaticParamsType and InferGetStaticPropsType type helpers * chore(astro): added changeset commit19fe4cb629
Author: Houston (Bot) <108291165+astrobot-houston@users.noreply.github.com> Date: Mon Mar 6 08:30:38 2023 -0800 [ci] release (#6414) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * Revert "Squashed commit of the following:" This reverts commitfbe129bf95
. * feat: add mdx backwards compat to make nonbreaking * chore: add comment on error code * Revert "chore: bump to minor changesets for MDX" This reverts commit3e997837bb
. --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
28e1224d12
commit
fec583909a
101 changed files with 3085 additions and 166 deletions
15
.changeset/breezy-cats-grab.md
Normal file
15
.changeset/breezy-cats-grab.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
'@astrojs/markdoc': patch
|
||||||
|
'@astrojs/mdx': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduce the (experimental) `@astrojs/markdoc` integration. This unlocks Markdoc inside your Content Collections, bringing support for Astro and UI components in your content. This also improves Astro core internals to make Content Collections extensible to more file types in the future.
|
||||||
|
|
||||||
|
You can install this integration using the `astro add` command:
|
||||||
|
|
||||||
|
```
|
||||||
|
astro add markdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
[Read the `@astrojs/markdoc` documentation](https://docs.astro.build/en/guides/integrations-guide/markdoc/) for usage instructions, and browse the [new `with-markdoc` starter](https://astro.new/with-markdoc) to try for yourself.
|
21
examples/with-markdoc/.gitignore
vendored
Normal file
21
examples/with-markdoc/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
4
examples/with-markdoc/.vscode/extensions.json
vendored
Normal file
4
examples/with-markdoc/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
examples/with-markdoc/.vscode/launch.json
vendored
Normal file
11
examples/with-markdoc/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
examples/with-markdoc/README.md
Normal file
59
examples/with-markdoc/README.md
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# Astro Example: Markdoc (experimental)
|
||||||
|
|
||||||
|
This starter showcases the experimental Markdoc integration.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm create astro@latest -- --template with-markdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-markdoc)
|
||||||
|
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-markdoc)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── content/
|
||||||
|
└── docs/
|
||||||
|
│ └── intro.mdoc
|
||||||
|
| └── config.ts
|
||||||
|
│ └── components/
|
||||||
|
| ├── Aside.astro
|
||||||
|
│ └── DocsContent.astro
|
||||||
|
│ └── layouts/
|
||||||
|
│ └── Layout.astro
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
| └── env.d.ts
|
||||||
|
├── astro.config.mjs
|
||||||
|
├── README.md
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Markdoc (`.mdoc`) files can be used in content collections to author your Markdown content alongside Astro and server-rendered UI framework components (React, Vue, Svelte, and more). See `src/content/docs/` for an example file.
|
||||||
|
|
||||||
|
You can also apply Astro components and server-rendered UI components (React, Vue, Svelte, etc) to your Markdoc files. See `src/content/DocsContent.astro` for an example.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :--------------------- | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
19
examples/with-markdoc/astro.config.mjs
Normal file
19
examples/with-markdoc/astro.config.mjs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
tags: {
|
||||||
|
aside: {
|
||||||
|
render: 'Aside',
|
||||||
|
attributes: {
|
||||||
|
type: { type: String },
|
||||||
|
title: { type: String },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
17
examples/with-markdoc/package.json
Normal file
17
examples/with-markdoc/package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "@example/with-markdoc",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "^0.0.0",
|
||||||
|
"astro": "^2.0.6"
|
||||||
|
}
|
||||||
|
}
|
13
examples/with-markdoc/public/favicon.svg
Normal file
13
examples/with-markdoc/public/favicon.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||||
|
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||||
|
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#000"/>
|
||||||
|
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<style>
|
||||||
|
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 873 B |
11
examples/with-markdoc/sandbox.config.json
Normal file
11
examples/with-markdoc/sandbox.config.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"infiniteLoopProtection": true,
|
||||||
|
"hardReloadOnChange": false,
|
||||||
|
"view": "browser",
|
||||||
|
"template": "node",
|
||||||
|
"container": {
|
||||||
|
"port": 3000,
|
||||||
|
"startScript": "start",
|
||||||
|
"node": "14"
|
||||||
|
}
|
||||||
|
}
|
116
examples/with-markdoc/src/components/Aside.astro
Normal file
116
examples/with-markdoc/src/components/Aside.astro
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
---
|
||||||
|
// Inspired by the `Aside` component from docs.astro.build
|
||||||
|
// https://github.com/withastro/docs/blob/main/src/components/Aside.astro
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type?: 'note' | 'tip' | 'caution' | 'danger';
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelByType = {
|
||||||
|
note: 'Note',
|
||||||
|
tip: 'Tip',
|
||||||
|
caution: 'Caution',
|
||||||
|
danger: 'Danger',
|
||||||
|
};
|
||||||
|
const { type = 'note' } = Astro.props as Props;
|
||||||
|
const title = Astro.props.title ?? labelByType[type] ?? '';
|
||||||
|
|
||||||
|
// SVG icon paths based on GitHub Octicons
|
||||||
|
const icons: Record<NonNullable<Props['type']>, { viewBox: string; d: string }> = {
|
||||||
|
note: {
|
||||||
|
viewBox: '0 0 18 18',
|
||||||
|
d: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z',
|
||||||
|
},
|
||||||
|
tip: {
|
||||||
|
viewBox: '0 0 18 18',
|
||||||
|
d: 'M14 0a8.8 8.8 0 0 0-6 2.6l-.5.4-.9 1H3.3a1.8 1.8 0 0 0-1.5.8L.1 7.6a.8.8 0 0 0 .4 1.1l3.1 1 .2.1 2.4 2.4.1.2 1 3a.8.8 0 0 0 1 .5l2.9-1.7a1.8 1.8 0 0 0 .8-1.5V9.5l1-1 .4-.4A8.8 8.8 0 0 0 16 2v-.1A1.8 1.8 0 0 0 14.2 0h-.1zm-3.5 10.6-.3.2L8 12.3l.5 1.8 2-1.2a.3.3 0 0 0 .1-.2v-2zM3.7 8.1l1.5-2.3.2-.3h-2a.3.3 0 0 0-.3.1l-1.2 2 1.8.5zm5.2-4.5a7.3 7.3 0 0 1 5.2-2.1h.1a.3.3 0 0 1 .3.3v.1a7.3 7.3 0 0 1-2.1 5.2l-.5.4a15.2 15.2 0 0 1-2.5 2L7.1 11 5 9l1.5-2.3a15.3 15.3 0 0 1 2-2.5l.4-.5zM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-8.4 9.6a1.5 1.5 0 1 0-2.2-2.2 7 7 0 0 0-1.1 3 .2.2 0 0 0 .3.3c.6 0 2.2-.4 3-1.1z',
|
||||||
|
},
|
||||||
|
caution: {
|
||||||
|
viewBox: '-1 1 18 18',
|
||||||
|
d: 'M8.9 1.5C8.7 1.2 8.4 1 8 1s-.7.2-.9.5l-7 12a1 1 0 0 0 0 1c.2.3.6.5 1 .5H15c.4 0 .7-.2.9-.5a1 1 0 0 0 0-1l-7-12zM9 13H7v-2h2v2zm0-3H7V6h2v4z',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
viewBox: '0 1 14 17',
|
||||||
|
d: 'M5 .3c.9 2.2.5 3.4-.5 4.3C3.5 5.6 2 6.5 1 8c-1.5 2-1.7 6.5 3.5 7.7-2.2-1.2-2.6-4.5-.3-6.6-.6 2 .6 3.3 2 2.8 1.4-.4 2.3.6 2.2 1.7 0 .8-.3 1.4-1 1.8A5.6 5.6 0 0 0 12 10c0-2.9-2.5-3.3-1.3-5.7-1.5.2-2 1.2-1.8 2.8 0 1-1 1.8-2 1.3-.6-.4-.6-1.2 0-1.8C8.2 5.3 8.7 2.5 5 .3z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { viewBox, d } = icons[type];
|
||||||
|
---
|
||||||
|
|
||||||
|
<aside class={`content ${type}`} aria-label={title}>
|
||||||
|
<p class="title" aria-hidden="true">
|
||||||
|
<span class="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} width={16} height={16}>
|
||||||
|
<path fill-rule="evenodd" d={d}></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<section>
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
aside {
|
||||||
|
--color-base-purple: 269, 79%;
|
||||||
|
--color-base-teal: 180, 80%;
|
||||||
|
--color-base-red: 351, 100%;
|
||||||
|
--color-base-yellow: 41, 100%;
|
||||||
|
|
||||||
|
--aside-color-base: var(--color-base-purple);
|
||||||
|
--aside-color-lightness: 54%;
|
||||||
|
--aside-accent-color: hsl(var(--aside-color-base), var(--aside-color-lightness));
|
||||||
|
--aside-text-lightness: 20%;
|
||||||
|
--aside-text-accent-color: hsl(var(--aside-color-base), var(--aside-text-lightness));
|
||||||
|
|
||||||
|
border-inline-start: 4px solid var(--aside-accent-color);
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: hsla(var(--aside-color-base), var(--aside-color-lightness), 0.1);
|
||||||
|
/* Indicates the aside boundaries for forced colors users, transparent is changed to a solid color */
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--aside-text-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside :global(a),
|
||||||
|
aside :global(a > code:not([class*='language'])) {
|
||||||
|
color: var(--aside-text-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside :global(pre) {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
--aside-color-lightness: 42%;
|
||||||
|
--aside-color-base: var(--color-base-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caution {
|
||||||
|
--aside-color-lightness: 59%;
|
||||||
|
--aside-color-base: var(--color-base-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
--aside-color-lightness: 54%;
|
||||||
|
--aside-color-base: var(--color-base-red);
|
||||||
|
}
|
||||||
|
</style>
|
32
examples/with-markdoc/src/components/DocsContent.astro
Normal file
32
examples/with-markdoc/src/components/DocsContent.astro
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
import Aside from './Aside.astro';
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<'docs'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content
|
||||||
|
components={{
|
||||||
|
// Pass a mapping from the component name
|
||||||
|
// To an Astro or UI component import
|
||||||
|
// See your `astro.config.mjs` for
|
||||||
|
// for the Markdoc tag mapping
|
||||||
|
Aside,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
table {
|
||||||
|
margin-block: 2rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding-block: 0.3rem;
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
9
examples/with-markdoc/src/content/config.ts
Normal file
9
examples/with-markdoc/src/content/config.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const docs = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { docs };
|
39
examples/with-markdoc/src/content/docs/intro.mdoc
Normal file
39
examples/with-markdoc/src/content/docs/intro.mdoc
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
title: Welcome to Markdoc 👋
|
||||||
|
---
|
||||||
|
|
||||||
|
This simple starter showcases Markdoc with Content Collections. All Markdoc features are supported, including this nifty built-in `{% table %}` tag:
|
||||||
|
|
||||||
|
{% table %}
|
||||||
|
* Feature
|
||||||
|
* Supported
|
||||||
|
---
|
||||||
|
* `.mdoc` in Content Collections
|
||||||
|
* ✅
|
||||||
|
---
|
||||||
|
* Markdoc transform configuration
|
||||||
|
* ✅
|
||||||
|
---
|
||||||
|
* Astro components
|
||||||
|
* ✅
|
||||||
|
{% /table %}
|
||||||
|
|
||||||
|
{% aside title="Code Challenge" type="tip" %}
|
||||||
|
|
||||||
|
Reveal the secret message below by adding `revealSecret: true` to your list of Markdoc variables.
|
||||||
|
|
||||||
|
_Hint: Try [adding a `variables` object](https://markdoc.dev/docs/variables#global-variables) to your Markdoc config. Check the `astro.config.mjs`._
|
||||||
|
|
||||||
|
{% if $revealSecret %}
|
||||||
|
|
||||||
|
Maybe the real secret was the Rick Rolls we shared along the way.
|
||||||
|
|
||||||
|
![Rick Astley dancing](https://media.tenor.com/x8v1oNUOmg4AAAAM/rickroll-roll.gif)
|
||||||
|
|
||||||
|
{% /if %}
|
||||||
|
|
||||||
|
{% /aside %}
|
||||||
|
|
||||||
|
Check out [the `@astrojs/markdoc` integration][astro-markdoc] for complete documentation and usage examples.
|
||||||
|
|
||||||
|
[astro-markdoc]: https://docs.astro.build/en/guides/integrations-guide/markdoc/
|
2
examples/with-markdoc/src/env.d.ts
vendored
Normal file
2
examples/with-markdoc/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
35
examples/with-markdoc/src/layouts/Layout.astro
Normal file
35
examples/with-markdoc/src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<title>{title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<style is:global>
|
||||||
|
html {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 60ch;
|
||||||
|
}
|
||||||
|
</style>
|
18
examples/with-markdoc/src/pages/index.astro
Normal file
18
examples/with-markdoc/src/pages/index.astro
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
import DocsContent from '../components/DocsContent.astro';
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
|
const intro = await getEntryBySlug('docs', 'intro');
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={intro.data.title}>
|
||||||
|
<main>
|
||||||
|
<h1>{intro.data.title}</h1>
|
||||||
|
<!-- `DocsContent` is a thin wrapper around -->
|
||||||
|
<!-- the `Content` component provided by Content Collections, -->
|
||||||
|
<!-- with added configuration for components. -->
|
||||||
|
<!-- This allows you to share global components wherever you render your Markdoc. -->
|
||||||
|
<DocsContent entry={intro} />
|
||||||
|
</main>
|
||||||
|
</Layout>
|
6
examples/with-markdoc/tsconfig.json
Normal file
6
examples/with-markdoc/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true
|
||||||
|
}
|
||||||
|
}
|
2
packages/astro/performance/.gitignore
vendored
Normal file
2
packages/astro/performance/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.astro/
|
||||||
|
fixtures/**/src/content/generated/
|
122
packages/astro/performance/content-benchmark.mjs
Normal file
122
packages/astro/performance/content-benchmark.mjs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { loadFixture } from '../test/test-utils.js';
|
||||||
|
import { generatePosts } from './scripts/generate-posts.mjs';
|
||||||
|
import yargs from 'yargs-parser';
|
||||||
|
import { cyan, bold, dim } from 'kleur/colors';
|
||||||
|
|
||||||
|
// Skip nonessential remark / rehype plugins for a fair comparison.
|
||||||
|
// This includes heading ID generation, syntax highlighting, GFM, and Smartypants.
|
||||||
|
process.env.ASTRO_PERFORMANCE_BENCHMARK = true;
|
||||||
|
|
||||||
|
const extByFixture = {
|
||||||
|
md: '.md',
|
||||||
|
mdx: '.mdx',
|
||||||
|
mdoc: '.mdoc',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function benchmark({ fixtures, templates, numPosts }) {
|
||||||
|
for (const fixture of fixtures) {
|
||||||
|
const root = new URL(`./fixtures/${fixture}/`, import.meta.url);
|
||||||
|
await generatePosts({
|
||||||
|
postsDir: fileURLToPath(new URL('./src/content/generated/', root)),
|
||||||
|
numPosts,
|
||||||
|
ext: extByFixture[fixture],
|
||||||
|
template: templates[fixture],
|
||||||
|
});
|
||||||
|
console.log(`[${fixture}] Generated posts`);
|
||||||
|
|
||||||
|
const { build } = await loadFixture({
|
||||||
|
root,
|
||||||
|
});
|
||||||
|
const now = performance.now();
|
||||||
|
console.log(`[${fixture}] Building...`);
|
||||||
|
await build();
|
||||||
|
console.log(cyan(`[${fixture}] Built in ${bold(getTimeStat(now, performance.now()))}.`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the build performance for content collections across multiple file types (md, mdx, mdoc)
|
||||||
|
(async function benchmarkAll() {
|
||||||
|
try {
|
||||||
|
const flags = yargs(process.argv.slice(2));
|
||||||
|
const test = Array.isArray(flags.test)
|
||||||
|
? flags.test
|
||||||
|
: typeof flags.test === 'string'
|
||||||
|
? [flags.test]
|
||||||
|
: ['simple', 'with-astro-components', 'with-react-components'];
|
||||||
|
|
||||||
|
const formats = Array.isArray(flags.format)
|
||||||
|
? flags.format
|
||||||
|
: typeof flags.format === 'string'
|
||||||
|
? [flags.format]
|
||||||
|
: ['md', 'mdx', 'mdoc'];
|
||||||
|
|
||||||
|
const numPosts = flags.numPosts || 1000;
|
||||||
|
|
||||||
|
if (test.includes('simple')) {
|
||||||
|
const fixtures = formats;
|
||||||
|
console.log(
|
||||||
|
`\n${bold('Simple')} ${dim(`${numPosts} posts (${formatsToString(fixtures)})`)}`
|
||||||
|
);
|
||||||
|
process.env.ASTRO_PERFORMANCE_TEST_NAME = 'simple';
|
||||||
|
await benchmark({
|
||||||
|
fixtures,
|
||||||
|
templates: {
|
||||||
|
md: 'simple.md',
|
||||||
|
mdx: 'simple.md',
|
||||||
|
mdoc: 'simple.md',
|
||||||
|
},
|
||||||
|
numPosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test.includes('with-astro-components')) {
|
||||||
|
const fixtures = formats.filter((format) => format !== 'md');
|
||||||
|
console.log(
|
||||||
|
`\n${bold('With Astro components')} ${dim(
|
||||||
|
`${numPosts} posts (${formatsToString(fixtures)})`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
process.env.ASTRO_PERFORMANCE_TEST_NAME = 'with-astro-components';
|
||||||
|
await benchmark({
|
||||||
|
fixtures,
|
||||||
|
templates: {
|
||||||
|
mdx: 'with-astro-components.mdx',
|
||||||
|
mdoc: 'with-astro-components.mdoc',
|
||||||
|
},
|
||||||
|
numPosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test.includes('with-react-components')) {
|
||||||
|
const fixtures = formats.filter((format) => format !== 'md');
|
||||||
|
console.log(
|
||||||
|
`\n${bold('With React components')} ${dim(
|
||||||
|
`${numPosts} posts (${formatsToString(fixtures)})`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
process.env.ASTRO_PERFORMANCE_TEST_NAME = 'with-react-components';
|
||||||
|
await benchmark({
|
||||||
|
fixtures,
|
||||||
|
templates: {
|
||||||
|
mdx: 'with-react-components.mdx',
|
||||||
|
mdoc: 'with-react-components.mdoc',
|
||||||
|
},
|
||||||
|
numPosts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.env.ASTRO_PERFORMANCE_BENCHMARK = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function getTimeStat(timeStart, timeEnd) {
|
||||||
|
const buildTime = timeEnd - timeStart;
|
||||||
|
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatsToString(formats) {
|
||||||
|
return formats.join(', ');
|
||||||
|
}
|
7
packages/astro/performance/fixtures/md/astro.config.mjs
Normal file
7
packages/astro/performance/fixtures/md/astro.config.mjs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [react()]
|
||||||
|
});
|
25
packages/astro/performance/fixtures/md/package.json
Normal file
25
packages/astro/performance/fixtures/md/package.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@performance/md",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "unlicensed",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/react": "^2.0.2",
|
||||||
|
"@performance/utils": "^0.0.0",
|
||||||
|
"@types/react": "^18.0.21",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"astro": "^2.0.12",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<'generated'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props as Props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content />
|
3
packages/astro/performance/fixtures/md/src/env.d.ts
vendored
Normal file
3
packages/astro/performance/fixtures/md/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference path="../../.astro/types.d.ts" />
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
12
packages/astro/performance/fixtures/md/src/pages/index.astro
Normal file
12
packages/astro/performance/fixtures/md/src/pages/index.astro
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { RenderContent } from '@performance/utils';
|
||||||
|
import ContentRenderer from '../ContentRenderer.astro';
|
||||||
|
|
||||||
|
const posts = await getCollection('generated');
|
||||||
|
---
|
||||||
|
|
||||||
|
<RenderContent
|
||||||
|
{posts}
|
||||||
|
renderer={ContentRenderer}
|
||||||
|
/>
|
7
packages/astro/performance/fixtures/md/tsconfig.json
Normal file
7
packages/astro/performance/fixtures/md/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
39
packages/astro/performance/fixtures/mdoc/astro.config.mjs
Normal file
39
packages/astro/performance/fixtures/mdoc/astro.config.mjs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from "@astrojs/markdoc";
|
||||||
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [markdoc({
|
||||||
|
nodes: process.env.ASTRO_PERFORMANCE_TEST_NAME === 'with-astro-components' ? {
|
||||||
|
heading: {
|
||||||
|
render: 'Heading',
|
||||||
|
attributes: {
|
||||||
|
level: { type: Number },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} : {},
|
||||||
|
tags: process.env.ASTRO_PERFORMANCE_TEST_NAME === 'with-astro-components' ? {
|
||||||
|
aside: {
|
||||||
|
render: 'Aside',
|
||||||
|
attributes: {
|
||||||
|
type: { type: String },
|
||||||
|
title: { type: String },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} : process.env.ASTRO_PERFORMANCE_TEST_NAME === 'with-react-components' ? {
|
||||||
|
'like-button': {
|
||||||
|
render: 'LikeButton',
|
||||||
|
attributes: {
|
||||||
|
liked: { type: Boolean },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'hydrated-like-button': {
|
||||||
|
render: 'HydratedLikeButton',
|
||||||
|
attributes: {
|
||||||
|
liked: { type: Boolean },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} : {},
|
||||||
|
}), react()],
|
||||||
|
});
|
26
packages/astro/performance/fixtures/mdoc/package.json
Normal file
26
packages/astro/performance/fixtures/mdoc/package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "@performance/mdoc",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "unlicensed",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "^0.0.0",
|
||||||
|
"@astrojs/react": "^2.0.2",
|
||||||
|
"@performance/utils": "^0.0.0",
|
||||||
|
"@types/react": "^18.0.21",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"astro": "^2.0.12",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import { Heading, Aside, LikeButton, HydratedLikeButton } from '@performance/utils';
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<'generated'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props as Props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
{entry.data.type === 'with-astro-components'
|
||||||
|
? <Content components={{ Heading, Aside }} />
|
||||||
|
: entry.data.type === 'with-react-components'
|
||||||
|
? <Content components={{ LikeButton, HydratedLikeButton }} />
|
||||||
|
: <Content />
|
||||||
|
}
|
2
packages/astro/performance/fixtures/mdoc/src/env.d.ts
vendored
Normal file
2
packages/astro/performance/fixtures/mdoc/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { RenderContent } from '@performance/utils';
|
||||||
|
import ContentRenderer from '../ContentRenderer.astro';
|
||||||
|
|
||||||
|
const posts = await getCollection('generated');
|
||||||
|
---
|
||||||
|
|
||||||
|
<RenderContent
|
||||||
|
{posts}
|
||||||
|
renderer={ContentRenderer}
|
||||||
|
/>
|
7
packages/astro/performance/fixtures/mdoc/tsconfig.json
Normal file
7
packages/astro/performance/fixtures/mdoc/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
8
packages/astro/performance/fixtures/mdx/astro.config.mjs
Normal file
8
packages/astro/performance/fixtures/mdx/astro.config.mjs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [mdx(), react()]
|
||||||
|
});
|
26
packages/astro/performance/fixtures/mdx/package.json
Normal file
26
packages/astro/performance/fixtures/mdx/package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "@performance/mdx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "unlicensed",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^0.17.2",
|
||||||
|
"@astrojs/react": "^2.0.2",
|
||||||
|
"@performance/utils": "^0.0.0",
|
||||||
|
"@types/react": "^18.0.21",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"astro": "^2.0.12",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
import Title from './Title.astro';
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<'generated'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props as Props;
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
{entry.data.type === 'with-astro-components'
|
||||||
|
? <Content components={{ h1: Title }} />
|
||||||
|
: <Content />
|
||||||
|
}
|
5
packages/astro/performance/fixtures/mdx/src/Title.astro
Normal file
5
packages/astro/performance/fixtures/mdx/src/Title.astro
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
import { Heading } from '@performance/utils';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Heading level={1}><slot /></Heading>
|
2
packages/astro/performance/fixtures/mdx/src/env.d.ts
vendored
Normal file
2
packages/astro/performance/fixtures/mdx/src/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { RenderContent } from '@performance/utils';
|
||||||
|
import ContentRenderer from '../ContentRenderer.astro';
|
||||||
|
|
||||||
|
const posts = await getCollection('generated');
|
||||||
|
---
|
||||||
|
|
||||||
|
<RenderContent
|
||||||
|
{posts}
|
||||||
|
renderer={ContentRenderer}
|
||||||
|
/>
|
7
packages/astro/performance/fixtures/mdx/tsconfig.json
Normal file
7
packages/astro/performance/fixtures/mdx/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
116
packages/astro/performance/fixtures/utils/Aside.astro
Normal file
116
packages/astro/performance/fixtures/utils/Aside.astro
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
---
|
||||||
|
// Inspired by the `Aside` component from docs.astro.build
|
||||||
|
// https://github.com/withastro/docs/blob/main/src/components/Aside.astro
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type?: 'note' | 'tip' | 'caution' | 'danger';
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelByType = {
|
||||||
|
note: 'Note',
|
||||||
|
tip: 'Tip',
|
||||||
|
caution: 'Caution',
|
||||||
|
danger: 'Danger',
|
||||||
|
};
|
||||||
|
const { type = 'note' } = Astro.props as Props;
|
||||||
|
const title = Astro.props.title ?? labelByType[type] ?? '';
|
||||||
|
|
||||||
|
// SVG icon paths based on GitHub Octicons
|
||||||
|
const icons: Record<NonNullable<Props['type']>, { viewBox: string; d: string }> = {
|
||||||
|
note: {
|
||||||
|
viewBox: '0 0 18 18',
|
||||||
|
d: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z',
|
||||||
|
},
|
||||||
|
tip: {
|
||||||
|
viewBox: '0 0 18 18',
|
||||||
|
d: 'M14 0a8.8 8.8 0 0 0-6 2.6l-.5.4-.9 1H3.3a1.8 1.8 0 0 0-1.5.8L.1 7.6a.8.8 0 0 0 .4 1.1l3.1 1 .2.1 2.4 2.4.1.2 1 3a.8.8 0 0 0 1 .5l2.9-1.7a1.8 1.8 0 0 0 .8-1.5V9.5l1-1 .4-.4A8.8 8.8 0 0 0 16 2v-.1A1.8 1.8 0 0 0 14.2 0h-.1zm-3.5 10.6-.3.2L8 12.3l.5 1.8 2-1.2a.3.3 0 0 0 .1-.2v-2zM3.7 8.1l1.5-2.3.2-.3h-2a.3.3 0 0 0-.3.1l-1.2 2 1.8.5zm5.2-4.5a7.3 7.3 0 0 1 5.2-2.1h.1a.3.3 0 0 1 .3.3v.1a7.3 7.3 0 0 1-2.1 5.2l-.5.4a15.2 15.2 0 0 1-2.5 2L7.1 11 5 9l1.5-2.3a15.3 15.3 0 0 1 2-2.5l.4-.5zM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-8.4 9.6a1.5 1.5 0 1 0-2.2-2.2 7 7 0 0 0-1.1 3 .2.2 0 0 0 .3.3c.6 0 2.2-.4 3-1.1z',
|
||||||
|
},
|
||||||
|
caution: {
|
||||||
|
viewBox: '-1 1 18 18',
|
||||||
|
d: 'M8.9 1.5C8.7 1.2 8.4 1 8 1s-.7.2-.9.5l-7 12a1 1 0 0 0 0 1c.2.3.6.5 1 .5H15c.4 0 .7-.2.9-.5a1 1 0 0 0 0-1l-7-12zM9 13H7v-2h2v2zm0-3H7V6h2v4z',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
viewBox: '0 1 14 17',
|
||||||
|
d: 'M5 .3c.9 2.2.5 3.4-.5 4.3C3.5 5.6 2 6.5 1 8c-1.5 2-1.7 6.5 3.5 7.7-2.2-1.2-2.6-4.5-.3-6.6-.6 2 .6 3.3 2 2.8 1.4-.4 2.3.6 2.2 1.7 0 .8-.3 1.4-1 1.8A5.6 5.6 0 0 0 12 10c0-2.9-2.5-3.3-1.3-5.7-1.5.2-2 1.2-1.8 2.8 0 1-1 1.8-2 1.3-.6-.4-.6-1.2 0-1.8C8.2 5.3 8.7 2.5 5 .3z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { viewBox, d } = icons[type];
|
||||||
|
---
|
||||||
|
|
||||||
|
<aside class={`content ${type}`} aria-label={title}>
|
||||||
|
<p class="title" aria-hidden="true">
|
||||||
|
<span class="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} width={16} height={16}>
|
||||||
|
<path fill-rule="evenodd" d={d}></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<section>
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
aside {
|
||||||
|
--color-base-purple: 269, 79%;
|
||||||
|
--color-base-teal: 180, 80%;
|
||||||
|
--color-base-red: 351, 100%;
|
||||||
|
--color-base-yellow: 41, 100%;
|
||||||
|
|
||||||
|
--aside-color-base: var(--color-base-purple);
|
||||||
|
--aside-color-lightness: 54%;
|
||||||
|
--aside-accent-color: hsl(var(--aside-color-base), var(--aside-color-lightness));
|
||||||
|
--aside-text-lightness: 20%;
|
||||||
|
--aside-text-accent-color: hsl(var(--aside-color-base), var(--aside-text-lightness));
|
||||||
|
|
||||||
|
border-inline-start: 4px solid var(--aside-accent-color);
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: hsla(var(--aside-color-base), var(--aside-color-lightness), 0.1);
|
||||||
|
/* Indicates the aside boundaries for forced colors users, transparent is changed to a solid color */
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--aside-text-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
fill: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside :global(a),
|
||||||
|
aside :global(a > code:not([class*='language'])) {
|
||||||
|
color: var(--aside-text-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside :global(pre) {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
--aside-color-lightness: 42%;
|
||||||
|
--aside-color-base: var(--color-base-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caution {
|
||||||
|
--aside-color-lightness: 59%;
|
||||||
|
--aside-color-base: var(--color-base-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
--aside-color-lightness: 54%;
|
||||||
|
--aside-color-base: var(--color-base-red);
|
||||||
|
}
|
||||||
|
</style>
|
20
packages/astro/performance/fixtures/utils/Heading.astro
Normal file
20
packages/astro/performance/fixtures/utils/Heading.astro
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
const { level } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{level === 1 && <h1><slot /></h1>}
|
||||||
|
{level === 2 && <h2><slot /></h2>}
|
||||||
|
{level === 3 && <h3><slot /></h3>}
|
||||||
|
{level === 4 && <h4><slot /></h4>}
|
||||||
|
{level === 5 && <h5><slot /></h5>}
|
||||||
|
{level === 6 && <h6><slot /></h6>}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: red;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import LikeButton from "./LikeButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
liked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Markdoc doesn't support `client:` directives -->
|
||||||
|
<!-- Use this Astro wrapper as a stand-in -->
|
||||||
|
<LikeButton client:load liked={Astro.props.liked} />
|
11
packages/astro/performance/fixtures/utils/LikeButton.tsx
Normal file
11
packages/astro/performance/fixtures/utils/LikeButton.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/** @jsxImportSource react */
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function LikeButton({ liked: likedInitial }: {liked: boolean}) {
|
||||||
|
const [liked, setLiked] = useState(likedInitial);
|
||||||
|
return (
|
||||||
|
<button onClick={() => setLiked(!liked)}>
|
||||||
|
{!liked ? <span>Like ❤️</span> : <span>Unlike 💔</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
posts: any[];
|
||||||
|
renderer: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts, renderer: ContentRenderer } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Md render test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{
|
||||||
|
posts.map((entry) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{entry.data.title}</h1>
|
||||||
|
<p>{entry.data.description}</p>
|
||||||
|
<ContentRenderer {entry} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</body>
|
||||||
|
</html>
|
6
packages/astro/performance/fixtures/utils/index.ts
Normal file
6
packages/astro/performance/fixtures/utils/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
export { default as RenderContent } from './RenderContent.astro';
|
||||||
|
export { default as Aside } from './Aside.astro';
|
||||||
|
export { default as Heading } from './Heading.astro';
|
||||||
|
export { default as LikeButton } from './LikeButton';
|
||||||
|
export { default as HydratedLikeButton } from './HydratedLikeButton.astro';
|
19
packages/astro/performance/fixtures/utils/package.json
Normal file
19
packages/astro/performance/fixtures/utils/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@performance/utils",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "unlicensed",
|
||||||
|
"devDependencies": {
|
||||||
|
"astro": "^2.0.12",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
6
packages/astro/performance/fixtures/utils/tsconfig.json
Normal file
6
packages/astro/performance/fixtures/utils/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
}
|
||||||
|
}
|
19
packages/astro/performance/package.json
Normal file
19
packages/astro/performance/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@test/performance",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"benchmark": "node ./content-benchmark.mjs"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/yargs-parser": "^21.0.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"kleur": "^4.1.5",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"yargs-parser": "^21.0.1"
|
||||||
|
}
|
||||||
|
}
|
18
packages/astro/performance/scripts/generate-posts.cli.mjs
Normal file
18
packages/astro/performance/scripts/generate-posts.cli.mjs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { generatePosts } from './generate-posts.mjs';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const postsDir = process.argv[2];
|
||||||
|
const numPosts =
|
||||||
|
typeof process.argv[3] === 'string' && process.argv[3].length > 0
|
||||||
|
? Number(process.argv[3])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const ext = process.argv[4];
|
||||||
|
const template = process.argv[5];
|
||||||
|
|
||||||
|
await generatePosts({ postsDir, numPosts, ext, template });
|
||||||
|
|
||||||
|
console.log(`${numPosts} ${ext} posts written to ${JSON.stringify(postsDir)} 🚀`);
|
||||||
|
})();
|
30
packages/astro/performance/scripts/generate-posts.mjs
Normal file
30
packages/astro/performance/scripts/generate-posts.mjs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const NUM_POSTS = 10;
|
||||||
|
const POSTS_DIR = './src/content/posts.generated';
|
||||||
|
const EXT = '.md';
|
||||||
|
const TEMPLATE = 'simple.md';
|
||||||
|
|
||||||
|
export async function generatePosts({
|
||||||
|
postsDir = POSTS_DIR,
|
||||||
|
numPosts = NUM_POSTS,
|
||||||
|
ext = EXT,
|
||||||
|
template = TEMPLATE,
|
||||||
|
}) {
|
||||||
|
if (fs.existsSync(postsDir)) {
|
||||||
|
const files = await fs.promises.readdir(postsDir);
|
||||||
|
await Promise.all(files.map((file) => fs.promises.unlink(path.join(postsDir, file))));
|
||||||
|
} else {
|
||||||
|
await fs.promises.mkdir(postsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(Array(numPosts).keys()).map((idx) => {
|
||||||
|
return fs.promises.writeFile(
|
||||||
|
`${postsDir}/post-${idx}${ext.startsWith('.') ? ext : `.${ext}`}`,
|
||||||
|
fs.readFileSync(new URL(`./templates/${template}`, import.meta.url), 'utf8')
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
43
packages/astro/performance/scripts/templates/simple.md
Normal file
43
packages/astro/performance/scripts/templates/simple.md
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
title: Simple
|
||||||
|
---
|
||||||
|
|
||||||
|
# Simple post
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur interdum quam vitae est dapibus auctor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus id purus vel ante interdum eleifend non sed magna. Nullam aliquet metus eget nunc pretium, ac malesuada elit ultricies. Quisque fermentum tellus sed risus tristique tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas eleifend odio sed tortor rhoncus venenatis. Maecenas dignissim convallis sem et sagittis. Aliquam facilisis auctor consectetur. Morbi vulputate fermentum lobortis. Aenean luctus risus erat, sit amet imperdiet lectus tempor et.
|
||||||
|
|
||||||
|
Aliquam erat volutpat. Vivamus sodales auctor hendrerit. Proin sollicitudin, neque id volutpat ultrices, urna tellus maximus quam, at placerat diam quam a nisl. In commodo, nibh quis rhoncus lacinia, felis nisi egestas tortor, dictum mollis magna massa at tortor. Cras tempus eleifend turpis, nec suscipit velit egestas a. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse nec nulla accumsan, sollicitudin dolor non, varius ipsum. Sed vel congue felis, sit amet bibendum neque. Pellentesque ut diam mollis augue auctor venenatis. Sed vitae aliquet lacus. Proin rutrum eget urna in vehicula. Vestibulum malesuada quis velit ac imperdiet. Donec interdum posuere nisl in auctor. Integer auctor pretium posuere.
|
||||||
|
|
||||||
|
Aenean tincidunt vitae augue id lacinia. Fusce a lorem accumsan, luctus magna non, fermentum arcu. Quisque mattis nibh ut ultrices vehicula. Fusce at porta mauris, eu sollicitudin augue. Curabitur tempor ante vulputate posuere interdum. Nam volutpat odio in blandit dapibus. Aliquam sit amet rutrum tortor.
|
||||||
|
|
||||||
|
Nulla eu odio nisl. Quisque malesuada arcu quis velit fermentum condimentum. Suspendisse potenti. Nullam non egestas sem. Sed et mi pharetra, ornare nunc ultricies, cursus est. Cras dignissim nisl eleifend nisl condimentum placerat. Vivamus tristique mattis vestibulum.
|
||||||
|
|
||||||
|
Maecenas at convallis dui. Pellentesque ac convallis libero. Mauris elementum arcu in quam pulvinar, a tincidunt dolor volutpat. Donec posuere eros ac nunc aliquam, non iaculis purus faucibus. Maecenas non lacus eu elit hendrerit fringilla eget vitae justo. Donec non lorem eu libero placerat volutpat. Vivamus euismod tristique lacus quis tincidunt.
|
||||||
|
|
||||||
|
Morbi a ligula eu odio dictum pharetra. Vestibulum a leo sit amet urna sodales facilisis posuere pretium lorem. Duis consectetur elementum sodales. Ut id libero quis dui laoreet faucibus eget ac felis. Suspendisse eu augue facilisis, consequat ex at, malesuada justo. Fusce tempor convallis orci a tristique. Pellentesque dapibus magna in sapien congue pharetra. Suspendisse potenti. Fusce in tortor justo. In hac habitasse platea dictumst. Pellentesque ligula odio, auctor vel consectetur quis, egestas a lectus. Sed arcu sapien, venenatis vitae nunc vitae, feugiat consequat elit.
|
||||||
|
|
||||||
|
Fusce bibendum odio tellus, ac consequat magna fringilla nec. Donec sed purus at magna pulvinar iaculis ac at nulla. Cras placerat, velit quis suscipit malesuada, eros dui ultrices sapien, sodales imperdiet enim ipsum vitae nisi. Mauris malesuada pretium nibh et luctus. Suspendisse potenti. In ante nibh, euismod at diam in, dapibus facilisis nunc. Suspendisse eleifend mollis dolor sit amet tristique. Nulla mattis tempor urna, nec pellentesque ante feugiat ut. Curabitur eleifend purus sed justo facilisis lacinia. Etiam maximus magna rhoncus quam tincidunt sollicitudin. Proin rhoncus metus lacus, non euismod mi gravida ac. Nam ac ipsum nec ante ultrices tempus ac mollis erat. Quisque ac tortor dolor. Integer eros mi, porttitor at rutrum ut, cursus sit amet ex. Pellentesque sed tortor vitae lorem malesuada gravida. Pellentesque bibendum ex nunc, non cursus lorem viverra gravida.
|
||||||
|
|
||||||
|
Integer lobortis erat quis dolor maximus porta. Sed ipsum est, maximus sit amet hendrerit ac, euismod quis nisi. Sed tincidunt, nisi sit amet varius tempus, turpis nisi sodales ante, sed ultricies urna neque vel purus. Maecenas sed laoreet tortor. Pellentesque enim massa, cursus in mauris vitae, facilisis egestas nisi. Mauris non ultrices purus, nec cursus diam. Proin eget ullamcorper augue. Pellentesque vehicula, sem vel dapibus tempus, neque tortor euismod libero, sed euismod diam enim id sapien. Sed mauris tellus, pretium eu ornare vitae, rhoncus at quam. Donec luctus mollis justo id rutrum. Aenean efficitur arcu nisi, non dignissim massa auctor et. In egestas lobortis nisi ac pharetra. Nam ultricies ipsum ut dui porta, sed commodo arcu vestibulum. Sed in felis molestie ante sodales auctor.
|
||||||
|
|
||||||
|
Praesent ac augue dui. Sed venenatis sit amet quam non rutrum. Vestibulum vitae tempor mi. Cras id luctus sapien, consectetur euismod magna. Nunc quis pellentesque sem, ut suscipit justo. Aliquam dignissim risus ante, vitae luctus enim vestibulum id. In hac habitasse platea dictumst. Nam rhoncus ante sed commodo porta. Ut lectus eros, porta sit amet velit vitae, elementum dignissim nulla. Cras nec scelerisque nulla. Quisque in diam eleifend, congue nulla eu, vestibulum magna. Sed vel purus elementum, mattis nunc id, mollis arcu. Pellentesque in pellentesque ipsum, non condimentum augue. Aenean tincidunt dui ut purus aliquet pretium. Integer vitae velit aliquet, tincidunt urna sed, bibendum lorem. Vivamus sit amet sapien ut sapien rhoncus fringilla vel a mi.
|
||||||
|
|
||||||
|
Praesent dignissim, arcu vel sollicitudin dictum, augue velit pretium ante, sit amet egestas velit lectus et tortor. In egestas ullamcorper risus, non vestibulum diam ultricies eu. Praesent a ex ac nisi consequat rhoncus. Fusce feugiat feugiat libero, vel lobortis mauris faucibus elementum. Mauris vitae luctus sapien. Etiam id pretium metus, in lacinia eros. Morbi et dictum risus. Morbi fringilla lorem ut elit fringilla blandit.
|
||||||
|
|
||||||
|
Nullam eu nibh ipsum. Curabitur aliquet varius ante, a pretium mauris dictum in. Integer nibh arcu, tristique ac sagittis nec, maximus et ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin blandit nec mi vel hendrerit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi consequat blandit orci, sed placerat sem fermentum sit amet. Quisque eu iaculis nisl. Suspendisse quam mauris, semper vel eleifend vitae, mollis in arcu.
|
||||||
|
|
||||||
|
Aenean porttitor blandit orci id bibendum. Nunc sit amet ligula bibendum, congue urna fringilla, dictum purus. Pellentesque blandit, nibh id laoreet placerat, mauris dui semper mi, id tincidunt metus massa nec nisi. Suspendisse potenti. Phasellus ut risus velit. Curabitur porttitor metus nunc, in malesuada justo gravida sit amet. Cras auctor justo magna, ut eleifend turpis dictum id. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque ut augue ac velit imperdiet suscipit non at massa.
|
||||||
|
|
||||||
|
Suspendisse euismod ornare quam id varius. Etiam ac velit id quam accumsan imperdiet sit amet eu nibh. Ut nec massa ultricies enim iaculis feugiat. Phasellus vehicula massa id ligula dapibus, sit amet viverra justo volutpat. Sed nunc est, efficitur et purus id, lacinia pellentesque metus. Pellentesque mi quam, maximus a blandit nec, mollis eget leo. Nulla sit amet elementum augue. Aenean id luctus nisl. Etiam non ante id augue dignissim suscipit in id quam. Quisque non consequat diam, eget condimentum turpis. Donec fringilla metus eget condimentum congue. Pellentesque aliquet blandit posuere. In bibendum ultrices ex a ornare. Donec quis efficitur metus. In commodo sollicitudin turpis et efficitur. Ut ac viverra nunc, sit amet varius sapien.
|
||||||
|
|
||||||
|
In sit amet felis et diam vehicula placerat. Nullam finibus lorem libero, et pretium eros consectetur euismod. In fringilla semper diam et hendrerit. Phasellus id erat at justo imperdiet aliquet. Donec dignissim auctor nunc, et ultrices ex rutrum nec. Aliquam ut cursus leo. Suspendisse semper velit ac lorem aliquet fermentum. Suspendisse congue mi et ultrices bibendum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec ac neque et enim facilisis posuere volutpat a augue. Vivamus feugiat fermentum rhoncus.
|
||||||
|
|
||||||
|
Proin ultricies turpis non mauris finibus elementum. Cras scelerisque pretium justo non efficitur. Curabitur at risus ut velit ullamcorper fringilla congue in nulla. Nunc laoreet lacinia purus at lobortis. Sed vulputate ex non cursus accumsan. Morbi risus elit, porttitor ac hendrerit sed, commodo suscipit nisi. Vivamus vestibulum ex sapien, sagittis blandit velit fermentum et.
|
||||||
|
|
||||||
|
Suspendisse ut ullamcorper ex, a hendrerit elit. Vivamus gravida tempor efficitur. Ut lobortis neque a mollis efficitur. Donec sit amet arcu quis massa fringilla consequat. Duis vitae nisl laoreet, suscipit tellus nec, malesuada sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus in sollicitudin nisi. Fusce id sapien ac nunc sagittis lobortis tempor auctor eros. Nunc lacinia enim vitae massa lacinia, suscipit facilisis turpis tempus. Integer nec mollis ex. Pellentesque magna nisl, dignissim in mi quis, malesuada elementum nibh. Duis consectetur erat quis interdum ornare. Phasellus lorem felis, aliquam a nunc at, luctus faucibus odio. In hac habitasse platea dictumst.
|
||||||
|
|
||||||
|
Vestibulum sit amet lorem arcu. Integer sed nisl ut turpis dapibus mollis sit amet sed turpis. Donec massa dolor, blandit at lacinia eu, ultricies eu turpis. Sed mollis non diam non consectetur. Morbi suscipit metus at orci sagittis ultricies. Mauris pulvinar maximus ex vitae convallis. Ut imperdiet vehicula mi ut imperdiet. Aliquam et dui at turpis volutpat condimentum. Morbi laoreet scelerisque leo, non tristique ante convallis vulputate. Nam et lorem enim. Cras cursus sodales nisi, nec facilisis felis feugiat sit amet. Aenean consequat pellentesque magna id venenatis. Nunc sed quam consequat, vestibulum diam nec, dignissim justo. Duis vulputate nibh sit amet tortor lobortis iaculis. Curabitur pellentesque dui sapien, nec varius libero hendrerit vel.
|
||||||
|
|
||||||
|
Curabitur quis mi ac massa hendrerit ornare id eget velit. Nulla dui lacus, hendrerit et fringilla sed, eleifend ut erat. Nunc ut fringilla ex, sit amet fringilla libero. Maecenas non ullamcorper orci. Duis posuere erat et urna rhoncus iaculis. Proin pellentesque porttitor nulla, non blandit ante semper vitae. Phasellus ut augue venenatis, tempus purus eu, efficitur massa. Etiam vel egestas tellus, ac pharetra lectus. Aliquam non commodo turpis. Quisque pharetra nunc et mauris bibendum, id vestibulum tellus fringilla. Nullam enim massa, porta id nisi at, accumsan sollicitudin elit. Morbi auctor lectus vitae orci cursus, et hendrerit odio accumsan. Pellentesque quis libero auctor, tempor dolor tempor, finibus arcu. Aliquam non cursus ex. Aliquam quis lacus ut purus pellentesque ultrices in a augue.
|
||||||
|
|
||||||
|
Morbi nunc diam, egestas sed condimentum a, interdum suscipit ligula. Morbi interdum dignissim imperdiet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris egestas, nulla nec feugiat porttitor, ex magna sodales nisl, ac volutpat tortor mauris vitae nibh. Cras cursus dignissim pretium. Nunc faucibus dui at lectus pellentesque vehicula. Maecenas tincidunt, libero quis hendrerit aliquet, tortor leo iaculis enim, sit amet ullamcorper tellus risus a orci. Donec dignissim metus in nulla eleifend molestie. Nunc at turpis et sem laoreet rutrum. Nulla facilisi. Sed luctus nisi sed egestas cursus.
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
type: with-astro-components
|
||||||
|
title: Post with Astro components
|
||||||
|
---
|
||||||
|
|
||||||
|
# This should be rendered with a title component
|
||||||
|
|
||||||
|
{% aside type="tip" %}
|
||||||
|
This is a tip component
|
||||||
|
{% /aside %}
|
||||||
|
{% aside type="note" %}
|
||||||
|
This is a note component
|
||||||
|
{% /aside %}
|
||||||
|
{% aside type="caution" %}
|
||||||
|
This is a caution component
|
||||||
|
{% /aside %}
|
||||||
|
{% aside type="danger" %}
|
||||||
|
This is a danger component
|
||||||
|
{% /aside %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
type: with-astro-components
|
||||||
|
title: Post with Astro components
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@performance/utils';
|
||||||
|
|
||||||
|
# This should be rendered with a title component
|
||||||
|
|
||||||
|
<Aside type="tip">This is a tip component</Aside>
|
||||||
|
<Aside type="note">This is a note component</Aside>
|
||||||
|
<Aside type="caution">This is a caution component</Aside>
|
||||||
|
<Aside type="danger">This is a danger component</Aside>
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
type: with-react-components
|
||||||
|
title: Post with React components
|
||||||
|
---
|
||||||
|
|
||||||
|
# This render clickable like components
|
||||||
|
|
||||||
|
{% like-button liked=true /%}
|
||||||
|
{% like-button liked=false /%}
|
||||||
|
|
||||||
|
{% hydrated-like-button liked=true /%}
|
||||||
|
{% hydrated-like-button liked=false /%}
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
type: with-react-components
|
||||||
|
title: Post with React components
|
||||||
|
---
|
||||||
|
|
||||||
|
import { LikeButton, HydratedLikeButton } from '@performance/utils';
|
||||||
|
|
||||||
|
# This render clickable like components
|
||||||
|
|
||||||
|
<LikeButton liked={true} />
|
||||||
|
<LikeButton liked={false} />
|
||||||
|
|
||||||
|
<HydratedLikeButton liked={true} />
|
||||||
|
<HydratedLikeButton liked={false} />
|
|
@ -1012,12 +1012,33 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
|
||||||
integrations: AstroIntegration[];
|
integrations: AstroIntegration[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContentEntryType {
|
||||||
|
extensions: string[];
|
||||||
|
getEntryInfo(params: {
|
||||||
|
fileUrl: URL;
|
||||||
|
contents: string;
|
||||||
|
}): GetEntryInfoReturnType | Promise<GetEntryInfoReturnType>;
|
||||||
|
contentModuleTypes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetEntryInfoReturnType = {
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Used for error hints to point to correct line and location
|
||||||
|
* Should be the untouched data as read from the file,
|
||||||
|
* including newlines
|
||||||
|
*/
|
||||||
|
rawData: string;
|
||||||
|
body: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AstroSettings {
|
export interface AstroSettings {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
|
|
||||||
adapter: AstroAdapter | undefined;
|
adapter: AstroAdapter | undefined;
|
||||||
injectedRoutes: InjectedRoute[];
|
injectedRoutes: InjectedRoute[];
|
||||||
pageExtensions: string[];
|
pageExtensions: string[];
|
||||||
|
contentEntryTypes: ContentEntryType[];
|
||||||
renderers: AstroRenderer[];
|
renderers: AstroRenderer[];
|
||||||
scripts: {
|
scripts: {
|
||||||
stage: InjectedScriptStage;
|
stage: InjectedScriptStage;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export const contentFileExts = ['.md', '.mdx'];
|
|
||||||
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
|
export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets';
|
||||||
export const CONTENT_FLAG = 'astroContent';
|
export const CONTENT_FLAG = 'astroContent';
|
||||||
export const VIRTUAL_MODULE_ID = 'astro:content';
|
export const VIRTUAL_MODULE_ID = 'astro:content';
|
||||||
|
|
|
@ -186,8 +186,8 @@ async function render({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Content,
|
Content,
|
||||||
headings: mod.getHeadings(),
|
headings: mod.getHeadings?.() ?? [],
|
||||||
remarkPluginFrontmatter: mod.frontmatter,
|
remarkPluginFrontmatter: mod.frontmatter ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
packages/astro/src/content/template/types.d.ts
vendored
20
packages/astro/src/content/template/types.d.ts
vendored
|
@ -1,7 +1,17 @@
|
||||||
|
declare module 'astro:content' {
|
||||||
|
interface Render {
|
||||||
|
'.md': Promise<{
|
||||||
|
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||||
|
headings: import('astro').MarkdownHeading[];
|
||||||
|
remarkPluginFrontmatter: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'astro:content' {
|
declare module 'astro:content' {
|
||||||
export { z } from 'astro/zod';
|
export { z } from 'astro/zod';
|
||||||
export type CollectionEntry<C extends keyof typeof entryMap> =
|
export type CollectionEntry<C extends keyof typeof entryMap> =
|
||||||
(typeof entryMap)[C][keyof (typeof entryMap)[C]] & Render;
|
(typeof entryMap)[C][keyof (typeof entryMap)[C]];
|
||||||
|
|
||||||
export const image: () => import('astro/zod').ZodObject<{
|
export const image: () => import('astro/zod').ZodObject<{
|
||||||
src: import('astro/zod').ZodString;
|
src: import('astro/zod').ZodString;
|
||||||
|
@ -64,14 +74,6 @@ declare module 'astro:content' {
|
||||||
Required<ContentConfig['collections'][C]>['schema']
|
Required<ContentConfig['collections'][C]>['schema']
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Render = {
|
|
||||||
render(): Promise<{
|
|
||||||
Content: import('astro').MarkdownInstance<{}>['Content'];
|
|
||||||
headings: import('astro').MarkdownHeading[];
|
|
||||||
remarkPluginFrontmatter: Record<string, any>;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const entryMap: {
|
const entryMap: {
|
||||||
// @@ENTRY_MAP@@
|
// @@ENTRY_MAP@@
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type fsMod from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { normalizePath, ViteDevServer } from 'vite';
|
import { normalizePath, ViteDevServer } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro.js';
|
import type { AstroSettings, ContentEntryType } from '../@types/astro.js';
|
||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import { info, LogOptions, warn } from '../core/logger/core.js';
|
import { info, LogOptions, warn } from '../core/logger/core.js';
|
||||||
import { isRelativePath } from '../core/path.js';
|
import { isRelativePath } from '../core/path.js';
|
||||||
|
@ -14,6 +14,7 @@ import {
|
||||||
ContentObservable,
|
ContentObservable,
|
||||||
ContentPaths,
|
ContentPaths,
|
||||||
EntryInfo,
|
EntryInfo,
|
||||||
|
getContentEntryExts,
|
||||||
getContentPaths,
|
getContentPaths,
|
||||||
getEntryInfo,
|
getEntryInfo,
|
||||||
getEntrySlug,
|
getEntrySlug,
|
||||||
|
@ -57,11 +58,12 @@ export async function createContentTypesGenerator({
|
||||||
}: CreateContentGeneratorParams) {
|
}: CreateContentGeneratorParams) {
|
||||||
const contentTypes: ContentTypes = {};
|
const contentTypes: ContentTypes = {};
|
||||||
const contentPaths = getContentPaths(settings.config, fs);
|
const contentPaths = getContentPaths(settings.config, fs);
|
||||||
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
|
|
||||||
let events: EventWithOptions[] = [];
|
let events: EventWithOptions[] = [];
|
||||||
let debounceTimeout: NodeJS.Timeout | undefined;
|
let debounceTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
const contentTypesBase = await fs.promises.readFile(contentPaths.typesTemplate, 'utf-8');
|
const typeTemplateContent = await fs.promises.readFile(contentPaths.typesTemplate, 'utf-8');
|
||||||
|
|
||||||
async function init(): Promise<
|
async function init(): Promise<
|
||||||
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
|
{ typesGenerated: true } | { typesGenerated: false; reason: 'no-content-dir' }
|
||||||
|
@ -121,7 +123,7 @@ export async function createContentTypesGenerator({
|
||||||
}
|
}
|
||||||
return { shouldGenerateTypes: true };
|
return { shouldGenerateTypes: true };
|
||||||
}
|
}
|
||||||
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths);
|
const fileType = getEntryType(fileURLToPath(event.entry), contentPaths, contentEntryExts);
|
||||||
if (fileType === 'ignored') {
|
if (fileType === 'ignored') {
|
||||||
return { shouldGenerateTypes: false };
|
return { shouldGenerateTypes: false };
|
||||||
}
|
}
|
||||||
|
@ -261,8 +263,9 @@ export async function createContentTypesGenerator({
|
||||||
fs,
|
fs,
|
||||||
contentTypes,
|
contentTypes,
|
||||||
contentPaths,
|
contentPaths,
|
||||||
contentTypesBase,
|
typeTemplateContent,
|
||||||
contentConfig: observable.status === 'loaded' ? observable.config : undefined,
|
contentConfig: observable.status === 'loaded' ? observable.config : undefined,
|
||||||
|
contentEntryTypes: settings.contentEntryTypes,
|
||||||
});
|
});
|
||||||
if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
|
if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
|
||||||
warnNonexistentCollections({
|
warnNonexistentCollections({
|
||||||
|
@ -300,7 +303,7 @@ async function parseSlug({
|
||||||
// on dev server startup or production build init.
|
// on dev server startup or production build init.
|
||||||
const rawContents = await fs.promises.readFile(event.entry, 'utf-8');
|
const rawContents = await fs.promises.readFile(event.entry, 'utf-8');
|
||||||
const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry));
|
const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry));
|
||||||
return getEntrySlug({ ...entryInfo, data: frontmatter });
|
return getEntrySlug({ ...entryInfo, unvalidatedSlug: frontmatter.slug });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEntry(
|
function setEntry(
|
||||||
|
@ -320,13 +323,15 @@ async function writeContentFiles({
|
||||||
fs,
|
fs,
|
||||||
contentPaths,
|
contentPaths,
|
||||||
contentTypes,
|
contentTypes,
|
||||||
contentTypesBase,
|
typeTemplateContent,
|
||||||
|
contentEntryTypes,
|
||||||
contentConfig,
|
contentConfig,
|
||||||
}: {
|
}: {
|
||||||
fs: typeof fsMod;
|
fs: typeof fsMod;
|
||||||
contentPaths: ContentPaths;
|
contentPaths: ContentPaths;
|
||||||
contentTypes: ContentTypes;
|
contentTypes: ContentTypes;
|
||||||
contentTypesBase: string;
|
typeTemplateContent: string;
|
||||||
|
contentEntryTypes: ContentEntryType[];
|
||||||
contentConfig?: ContentConfig;
|
contentConfig?: ContentConfig;
|
||||||
}) {
|
}) {
|
||||||
let contentTypesStr = '';
|
let contentTypesStr = '';
|
||||||
|
@ -338,8 +343,11 @@ async function writeContentFiles({
|
||||||
for (const entryKey of entryKeys) {
|
for (const entryKey of entryKeys) {
|
||||||
const entryMetadata = contentTypes[collectionKey][entryKey];
|
const entryMetadata = contentTypes[collectionKey][entryKey];
|
||||||
const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
|
const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
|
||||||
|
const renderType = `{ render(): Render[${JSON.stringify(
|
||||||
|
path.extname(JSON.parse(entryKey))
|
||||||
|
)}] }`;
|
||||||
const slugType = JSON.stringify(entryMetadata.slug);
|
const slugType = JSON.stringify(entryMetadata.slug);
|
||||||
contentTypesStr += `${entryKey}: {\n id: ${entryKey},\n slug: ${slugType},\n body: string,\n collection: ${collectionKey},\n data: ${dataType}\n},\n`;
|
contentTypesStr += `${entryKey}: {\n id: ${entryKey},\n slug: ${slugType},\n body: string,\n collection: ${collectionKey},\n data: ${dataType}\n} & ${renderType},\n`;
|
||||||
}
|
}
|
||||||
contentTypesStr += `},\n`;
|
contentTypesStr += `},\n`;
|
||||||
}
|
}
|
||||||
|
@ -359,13 +367,21 @@ async function writeContentFiles({
|
||||||
configPathRelativeToCacheDir = configPathRelativeToCacheDir.replace(/\.ts$/, '');
|
configPathRelativeToCacheDir = configPathRelativeToCacheDir.replace(/\.ts$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
contentTypesBase = contentTypesBase.replace('// @@ENTRY_MAP@@', contentTypesStr);
|
for (const contentEntryType of contentEntryTypes) {
|
||||||
contentTypesBase = contentTypesBase.replace(
|
if (contentEntryType.contentModuleTypes) {
|
||||||
|
typeTemplateContent = contentEntryType.contentModuleTypes + '\n' + typeTemplateContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeTemplateContent = typeTemplateContent.replace('// @@ENTRY_MAP@@', contentTypesStr);
|
||||||
|
typeTemplateContent = typeTemplateContent.replace(
|
||||||
"'@@CONTENT_CONFIG_TYPE@@'",
|
"'@@CONTENT_CONFIG_TYPE@@'",
|
||||||
contentConfig ? `typeof import(${JSON.stringify(configPathRelativeToCacheDir)})` : 'never'
|
contentConfig ? `typeof import(${JSON.stringify(configPathRelativeToCacheDir)})` : 'never'
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.promises.writeFile(new URL(CONTENT_TYPES_FILE, contentPaths.cacheDir), contentTypesBase);
|
await fs.promises.writeFile(
|
||||||
|
new URL(CONTENT_TYPES_FILE, contentPaths.cacheDir),
|
||||||
|
typeTemplateContent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function warnNonexistentCollections({
|
function warnNonexistentCollections({
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { z } from 'zod';
|
||||||
import { AstroConfig, AstroSettings } from '../@types/astro.js';
|
import { AstroConfig, AstroSettings } from '../@types/astro.js';
|
||||||
import type { ImageMetadata } from '../assets/types.js';
|
import type { ImageMetadata } from '../assets/types.js';
|
||||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||||
import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
|
import { CONTENT_TYPES_FILE } from './consts.js';
|
||||||
|
|
||||||
export const collectionConfigParser = z.object({
|
export const collectionConfigParser = z.object({
|
||||||
schema: z.any().optional(),
|
schema: z.any().optional(),
|
||||||
|
@ -30,14 +30,7 @@ export const contentConfigParser = z.object({
|
||||||
export type CollectionConfig = z.infer<typeof collectionConfigParser>;
|
export type CollectionConfig = z.infer<typeof collectionConfigParser>;
|
||||||
export type ContentConfig = z.infer<typeof contentConfigParser>;
|
export type ContentConfig = z.infer<typeof contentConfigParser>;
|
||||||
|
|
||||||
type Entry = {
|
type EntryInternal = { rawData: string; filePath: string };
|
||||||
id: string;
|
|
||||||
collection: string;
|
|
||||||
slug: string;
|
|
||||||
data: any;
|
|
||||||
body: string;
|
|
||||||
_internal: { rawData: string; filePath: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EntryInfo = {
|
export type EntryInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -71,10 +64,10 @@ export function getEntrySlug({
|
||||||
id,
|
id,
|
||||||
collection,
|
collection,
|
||||||
slug,
|
slug,
|
||||||
data: unparsedData,
|
unvalidatedSlug,
|
||||||
}: Pick<Entry, 'id' | 'collection' | 'slug' | 'data'>) {
|
}: EntryInfo & { unvalidatedSlug?: unknown }) {
|
||||||
try {
|
try {
|
||||||
return z.string().default(slug).parse(unparsedData.slug);
|
return z.string().default(slug).parse(unvalidatedSlug);
|
||||||
} catch {
|
} catch {
|
||||||
throw new AstroError({
|
throw new AstroError({
|
||||||
...AstroErrorData.InvalidContentEntrySlugError,
|
...AstroErrorData.InvalidContentEntrySlugError,
|
||||||
|
@ -83,9 +76,12 @@ export function getEntrySlug({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntryData(entry: Entry, collectionConfig: CollectionConfig) {
|
export async function getEntryData(
|
||||||
|
entry: EntryInfo & { unvalidatedData: Record<string, unknown>; _internal: EntryInternal },
|
||||||
|
collectionConfig: CollectionConfig
|
||||||
|
) {
|
||||||
// Remove reserved `slug` field before parsing data
|
// Remove reserved `slug` field before parsing data
|
||||||
let { slug, ...data } = entry.data;
|
let { slug, ...data } = entry.unvalidatedData;
|
||||||
if (collectionConfig.schema) {
|
if (collectionConfig.schema) {
|
||||||
// TODO: remove for 2.0 stable release
|
// TODO: remove for 2.0 stable release
|
||||||
if (
|
if (
|
||||||
|
@ -112,7 +108,9 @@ export async function getEntryData(entry: Entry, collectionConfig: CollectionCon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Use `safeParseAsync` to allow async transforms
|
// Use `safeParseAsync` to allow async transforms
|
||||||
const parsed = await collectionConfig.schema.safeParseAsync(entry.data, { errorMap });
|
const parsed = await collectionConfig.schema.safeParseAsync(entry.unvalidatedData, {
|
||||||
|
errorMap,
|
||||||
|
});
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
data = parsed.data;
|
data = parsed.data;
|
||||||
} else {
|
} else {
|
||||||
|
@ -138,6 +136,10 @@ export async function getEntryData(entry: Entry, collectionConfig: CollectionCon
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryTypes'>) {
|
||||||
|
return settings.contentEntryTypes.map((t) => t.extensions).flat();
|
||||||
|
}
|
||||||
|
|
||||||
export class NoCollectionError extends Error {}
|
export class NoCollectionError extends Error {}
|
||||||
|
|
||||||
export function getEntryInfo(
|
export function getEntryInfo(
|
||||||
|
@ -178,14 +180,15 @@ export function getEntryInfo({
|
||||||
|
|
||||||
export function getEntryType(
|
export function getEntryType(
|
||||||
entryPath: string,
|
entryPath: string,
|
||||||
paths: Pick<ContentPaths, 'config' | 'contentDir'>
|
paths: Pick<ContentPaths, 'config' | 'contentDir'>,
|
||||||
|
contentFileExts: string[]
|
||||||
): 'content' | 'config' | 'ignored' | 'unsupported' {
|
): 'content' | 'config' | 'ignored' | 'unsupported' {
|
||||||
const { ext, base } = path.parse(entryPath);
|
const { ext, base } = path.parse(entryPath);
|
||||||
const fileUrl = pathToFileURL(entryPath);
|
const fileUrl = pathToFileURL(entryPath);
|
||||||
|
|
||||||
if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir) || isOnIgnoreList(base)) {
|
if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir) || isOnIgnoreList(base)) {
|
||||||
return 'ignored';
|
return 'ignored';
|
||||||
} else if ((contentFileExts as readonly string[]).includes(ext)) {
|
} else if (contentFileExts.includes(ext)) {
|
||||||
return 'content';
|
return 'content';
|
||||||
} else if (fileUrl.href === paths.config.url.href) {
|
} else if (fileUrl.href === paths.config.url.href) {
|
||||||
return 'config';
|
return 'config';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import npath from 'node:path';
|
import npath from 'node:path';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
|
import { AstroSettings } from '../@types/astro.js';
|
||||||
import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js';
|
import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js';
|
||||||
import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js';
|
import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js';
|
||||||
import { AstroBuildPlugin } from '../core/build/plugin.js';
|
import { AstroBuildPlugin } from '../core/build/plugin.js';
|
||||||
|
@ -11,23 +12,30 @@ import { prependForwardSlash } from '../core/path.js';
|
||||||
import { getStylesForURL } from '../core/render/dev/css.js';
|
import { getStylesForURL } from '../core/render/dev/css.js';
|
||||||
import { getScriptsForURL } from '../core/render/dev/scripts.js';
|
import { getScriptsForURL } from '../core/render/dev/scripts.js';
|
||||||
import {
|
import {
|
||||||
contentFileExts,
|
|
||||||
LINKS_PLACEHOLDER,
|
LINKS_PLACEHOLDER,
|
||||||
PROPAGATED_ASSET_FLAG,
|
PROPAGATED_ASSET_FLAG,
|
||||||
SCRIPTS_PLACEHOLDER,
|
SCRIPTS_PLACEHOLDER,
|
||||||
STYLES_PLACEHOLDER,
|
STYLES_PLACEHOLDER,
|
||||||
} from './consts.js';
|
} from './consts.js';
|
||||||
|
import { getContentEntryExts } from './utils.js';
|
||||||
|
|
||||||
function isPropagatedAsset(viteId: string): boolean {
|
function isPropagatedAsset(viteId: string, contentEntryExts: string[]): boolean {
|
||||||
const url = new URL(viteId, 'file://');
|
const url = new URL(viteId, 'file://');
|
||||||
return (
|
return (
|
||||||
url.searchParams.has(PROPAGATED_ASSET_FLAG) &&
|
url.searchParams.has(PROPAGATED_ASSET_FLAG) &&
|
||||||
contentFileExts.some((ext) => url.pathname.endsWith(ext))
|
contentEntryExts.some((ext) => url.pathname.endsWith(ext))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function astroContentAssetPropagationPlugin({ mode }: { mode: string }): Plugin {
|
export function astroContentAssetPropagationPlugin({
|
||||||
|
mode,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
mode: string;
|
||||||
|
settings: AstroSettings;
|
||||||
|
}): Plugin {
|
||||||
let devModuleLoader: ModuleLoader;
|
let devModuleLoader: ModuleLoader;
|
||||||
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
return {
|
return {
|
||||||
name: 'astro:content-asset-propagation',
|
name: 'astro:content-asset-propagation',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
|
@ -37,7 +45,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(id) {
|
load(id) {
|
||||||
if (isPropagatedAsset(id)) {
|
if (isPropagatedAsset(id, contentEntryExts)) {
|
||||||
const basePath = id.split('?')[0];
|
const basePath = id.split('?')[0];
|
||||||
const code = `
|
const code = `
|
||||||
export async function getMod() {
|
export async function getMod() {
|
||||||
|
@ -52,7 +60,7 @@ export function astroContentAssetPropagationPlugin({ mode }: { mode: string }):
|
||||||
},
|
},
|
||||||
async transform(code, id, options) {
|
async transform(code, id, options) {
|
||||||
if (!options?.ssr) return;
|
if (!options?.ssr) return;
|
||||||
if (devModuleLoader && isPropagatedAsset(id)) {
|
if (devModuleLoader && isPropagatedAsset(id, contentEntryExts)) {
|
||||||
const basePath = id.split('?')[0];
|
const basePath = id.split('?')[0];
|
||||||
if (!devModuleLoader.getModuleById(basePath)?.ssrModule) {
|
if (!devModuleLoader.getModuleById(basePath)?.ssrModule) {
|
||||||
await devModuleLoader.import(basePath);
|
await devModuleLoader.import(basePath);
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import * as devalue from 'devalue';
|
import * as devalue from 'devalue';
|
||||||
import type fsMod from 'node:fs';
|
import type fsMod from 'node:fs';
|
||||||
|
import { extname } from 'node:path';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import { AstroSettings } from '../@types/astro.js';
|
import { AstroSettings, ContentEntryType } from '../@types/astro.js';
|
||||||
import { AstroErrorData } from '../core/errors/errors-data.js';
|
import { AstroErrorData } from '../core/errors/errors-data.js';
|
||||||
import { AstroError } from '../core/errors/errors.js';
|
import { AstroError } from '../core/errors/errors.js';
|
||||||
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
import { contentFileExts, CONTENT_FLAG } from './consts.js';
|
import { CONTENT_FLAG } from './consts.js';
|
||||||
import {
|
import {
|
||||||
ContentConfig,
|
ContentConfig,
|
||||||
|
getContentEntryExts,
|
||||||
extractFrontmatterAssets,
|
extractFrontmatterAssets,
|
||||||
getContentPaths,
|
getContentPaths,
|
||||||
getEntryData,
|
getEntryData,
|
||||||
|
@ -20,9 +22,9 @@ import {
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
function isContentFlagImport(viteId: string) {
|
function isContentFlagImport(viteId: string, contentEntryExts: string[]) {
|
||||||
const { pathname, searchParams } = new URL(viteId, 'file://');
|
const { searchParams, pathname } = new URL(viteId, 'file://');
|
||||||
return searchParams.has(CONTENT_FLAG) && contentFileExts.some((ext) => pathname.endsWith(ext));
|
return searchParams.has(CONTENT_FLAG) && contentEntryExts.some((ext) => pathname.endsWith(ext));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function astroContentImportPlugin({
|
export function astroContentImportPlugin({
|
||||||
|
@ -33,12 +35,20 @@ export function astroContentImportPlugin({
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
}): Plugin {
|
}): Plugin {
|
||||||
const contentPaths = getContentPaths(settings.config, fs);
|
const contentPaths = getContentPaths(settings.config, fs);
|
||||||
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
|
|
||||||
|
const contentEntryExtToParser: Map<string, ContentEntryType> = new Map();
|
||||||
|
for (const entryType of settings.contentEntryTypes) {
|
||||||
|
for (const ext of entryType.extensions) {
|
||||||
|
contentEntryExtToParser.set(ext, entryType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'astro:content-imports',
|
name: 'astro:content-imports',
|
||||||
async load(id) {
|
async load(id) {
|
||||||
const { fileId } = getFileInfo(id, settings.config);
|
const { fileId } = getFileInfo(id, settings.config);
|
||||||
if (isContentFlagImport(id)) {
|
if (isContentFlagImport(id, contentEntryExts)) {
|
||||||
const observable = globalContentConfigObserver.get();
|
const observable = globalContentConfigObserver.get();
|
||||||
|
|
||||||
// Content config should be loaded before this plugin is used
|
// Content config should be loaded before this plugin is used
|
||||||
|
@ -71,44 +81,55 @@ export function astroContentImportPlugin({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
|
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
|
||||||
const {
|
const fileExt = extname(fileId);
|
||||||
content: body,
|
if (!contentEntryExtToParser.has(fileExt)) {
|
||||||
data: unparsedData,
|
throw new AstroError({
|
||||||
matter: rawData = '',
|
...AstroErrorData.UnknownContentCollectionError,
|
||||||
} = parseFrontmatter(rawContents, fileId);
|
message: `No parser found for content entry ${JSON.stringify(
|
||||||
const entryInfo = getEntryInfo({
|
fileId
|
||||||
|
)}. Did you apply an integration for this file type?`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const contentEntryParser = contentEntryExtToParser.get(fileExt)!;
|
||||||
|
const info = await contentEntryParser.getEntryInfo({
|
||||||
|
fileUrl: pathToFileURL(fileId),
|
||||||
|
contents: rawContents,
|
||||||
|
});
|
||||||
|
const generatedInfo = getEntryInfo({
|
||||||
entry: pathToFileURL(fileId),
|
entry: pathToFileURL(fileId),
|
||||||
contentDir: contentPaths.contentDir,
|
contentDir: contentPaths.contentDir,
|
||||||
});
|
});
|
||||||
if (entryInfo instanceof Error) return;
|
if (generatedInfo instanceof Error) return;
|
||||||
|
|
||||||
const _internal = { filePath: fileId, rawData };
|
const _internal = { filePath: fileId, rawData: info.rawData };
|
||||||
const partialEntry = { data: unparsedData, body, _internal, ...entryInfo };
|
|
||||||
// TODO: move slug calculation to the start of the build
|
// TODO: move slug calculation to the start of the build
|
||||||
// to generate a performant lookup map for `getEntryBySlug`
|
// to generate a performant lookup map for `getEntryBySlug`
|
||||||
const slug = getEntrySlug(partialEntry);
|
const slug = getEntrySlug({ ...generatedInfo, unvalidatedSlug: info.slug });
|
||||||
|
|
||||||
const collectionConfig = contentConfig?.collections[entryInfo.collection];
|
const collectionConfig = contentConfig?.collections[generatedInfo.collection];
|
||||||
const data = collectionConfig
|
const data = collectionConfig
|
||||||
? await getEntryData(partialEntry, collectionConfig)
|
? await getEntryData(
|
||||||
: unparsedData;
|
{ ...generatedInfo, _internal, unvalidatedData: info.data },
|
||||||
|
collectionConfig
|
||||||
|
)
|
||||||
|
: info.data;
|
||||||
|
|
||||||
const images = extractFrontmatterAssets(data).map(
|
const images = extractFrontmatterAssets(data).map(
|
||||||
(image) => `'${image}': await import('${normalizePath(image)}'),`
|
(image) => `'${image}': await import('${normalizePath(image)}'),`
|
||||||
);
|
);
|
||||||
|
|
||||||
const code = escapeViteEnvReferences(`
|
const code = escapeViteEnvReferences(`
|
||||||
export const id = ${JSON.stringify(entryInfo.id)};
|
export const id = ${JSON.stringify(generatedInfo.id)};
|
||||||
export const collection = ${JSON.stringify(entryInfo.collection)};
|
export const collection = ${JSON.stringify(generatedInfo.collection)};
|
||||||
export const slug = ${JSON.stringify(slug)};
|
export const slug = ${JSON.stringify(slug)};
|
||||||
export const body = ${JSON.stringify(body)};
|
export const body = ${JSON.stringify(info.body)};
|
||||||
const frontmatterImages = {
|
const frontmatterImages = {
|
||||||
${images.join('\n')}
|
${images.join('\n')}
|
||||||
}
|
}
|
||||||
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
|
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
|
||||||
export const _internal = {
|
export const _internal = {
|
||||||
filePath: ${JSON.stringify(fileId)},
|
filePath: ${JSON.stringify(_internal.filePath)},
|
||||||
rawData: ${JSON.stringify(rawData)},
|
rawData: ${JSON.stringify(_internal.rawData)},
|
||||||
};
|
};
|
||||||
`);
|
`);
|
||||||
return { code };
|
return { code };
|
||||||
|
@ -118,11 +139,11 @@ export const _internal = {
|
||||||
viteServer.watcher.on('all', async (event, entry) => {
|
viteServer.watcher.on('all', async (event, entry) => {
|
||||||
if (
|
if (
|
||||||
['add', 'unlink', 'change'].includes(event) &&
|
['add', 'unlink', 'change'].includes(event) &&
|
||||||
getEntryType(entry, contentPaths) === 'config'
|
getEntryType(entry, contentPaths, contentEntryExts) === 'config'
|
||||||
) {
|
) {
|
||||||
// Content modules depend on config, so we need to invalidate them.
|
// Content modules depend on config, so we need to invalidate them.
|
||||||
for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
|
for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
|
||||||
if (isContentFlagImport(modUrl)) {
|
if (isContentFlagImport(modUrl, contentEntryExts)) {
|
||||||
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
|
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
|
||||||
if (mod) {
|
if (mod) {
|
||||||
viteServer.moduleGraph.invalidateModule(mod);
|
viteServer.moduleGraph.invalidateModule(mod);
|
||||||
|
@ -133,7 +154,7 @@ export const _internal = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async transform(code, id) {
|
async transform(code, id) {
|
||||||
if (isContentFlagImport(id)) {
|
if (isContentFlagImport(id, contentEntryExts)) {
|
||||||
// Escape before Rollup internal transform.
|
// Escape before Rollup internal transform.
|
||||||
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
|
// Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
|
||||||
return { code: escapeViteEnvReferences(code) };
|
return { code: escapeViteEnvReferences(code) };
|
||||||
|
|
|
@ -4,8 +4,8 @@ import type { Plugin } from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro.js';
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
|
import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
|
||||||
import { contentFileExts, VIRTUAL_MODULE_ID } from './consts.js';
|
import { VIRTUAL_MODULE_ID } from './consts.js';
|
||||||
import { getContentPaths } from './utils.js';
|
import { getContentEntryExts, getContentPaths } from './utils.js';
|
||||||
|
|
||||||
interface AstroContentVirtualModPluginParams {
|
interface AstroContentVirtualModPluginParams {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -22,11 +22,17 @@ export function astroContentVirtualModPlugin({
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const contentEntryExts = getContentEntryExts(settings);
|
||||||
|
|
||||||
const assetsDir = settings.config.experimental.assets
|
const assetsDir = settings.config.experimental.assets
|
||||||
? contentPaths.assetsDir.toString()
|
? contentPaths.assetsDir.toString()
|
||||||
: 'undefined';
|
: 'undefined';
|
||||||
const entryGlob = `${relContentDir}**/*{${contentFileExts.join(',')}}`;
|
const extGlob =
|
||||||
|
contentEntryExts.length === 1
|
||||||
|
? // Wrapping {...} breaks when there is only one extension
|
||||||
|
contentEntryExts[0]
|
||||||
|
: `{${contentEntryExts.join(',')}}`;
|
||||||
|
const entryGlob = `${relContentDir}**/*${extGlob}`;
|
||||||
const virtualModContents = fsMod
|
const virtualModContents = fsMod
|
||||||
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
|
.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
|
||||||
.replace('@@CONTENT_DIR@@', relContentDir)
|
.replace('@@CONTENT_DIR@@', relContentDir)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import jsxRenderer from '../../jsx/renderer.js';
|
||||||
import { createDefaultDevConfig } from './config.js';
|
import { createDefaultDevConfig } from './config.js';
|
||||||
import { AstroTimer } from './timer.js';
|
import { AstroTimer } from './timer.js';
|
||||||
import { loadTSConfig } from './tsconfig.js';
|
import { loadTSConfig } from './tsconfig.js';
|
||||||
|
import { markdownContentEntryType } from '../../vite-plugin-markdown/content-entry-type.js';
|
||||||
|
|
||||||
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
return {
|
return {
|
||||||
|
@ -19,6 +20,7 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
? [{ pattern: '/_image', entryPoint: 'astro/assets/image-endpoint' }]
|
||||||
: [],
|
: [],
|
||||||
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
||||||
|
contentEntryTypes: [markdownContentEntryType],
|
||||||
renderers: [jsxRenderer],
|
renderers: [jsxRenderer],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
watchFiles: [],
|
watchFiles: [],
|
||||||
|
|
|
@ -116,7 +116,7 @@ export async function createVite(
|
||||||
astroInjectEnvTsPlugin({ settings, logging, fs }),
|
astroInjectEnvTsPlugin({ settings, logging, fs }),
|
||||||
astroContentVirtualModPlugin({ settings }),
|
astroContentVirtualModPlugin({ settings }),
|
||||||
astroContentImportPlugin({ fs, settings }),
|
astroContentImportPlugin({ fs, settings }),
|
||||||
astroContentAssetPropagationPlugin({ mode }),
|
astroContentAssetPropagationPlugin({ mode, settings }),
|
||||||
vitePluginSSRManifest(),
|
vitePluginSSRManifest(),
|
||||||
settings.config.experimental.assets ? [astroAssetsPlugin({ settings, logging, mode })] : [],
|
settings.config.experimental.assets ? [astroAssetsPlugin({ settings, logging, mode })] : [],
|
||||||
],
|
],
|
||||||
|
|
|
@ -122,6 +122,10 @@ export interface AstroErrorPayload {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shiki does not support `mjs` or `cjs` aliases by default.
|
||||||
|
// Map these to `.js` during error highlighting.
|
||||||
|
const ALTERNATIVE_JS_EXTS = ['cjs', 'mjs'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a payload for Vite's error overlay
|
* Generate a payload for Vite's error overlay
|
||||||
*/
|
*/
|
||||||
|
@ -150,9 +154,13 @@ export async function getViteErrorPayload(err: ErrorWithMetadata): Promise<Astro
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const highlighter = await getHighlighter({ theme: 'css-variables' });
|
const highlighter = await getHighlighter({ theme: 'css-variables' });
|
||||||
|
let highlighterLang = err.loc?.file?.split('.').pop();
|
||||||
|
if (ALTERNATIVE_JS_EXTS.includes(highlighterLang ?? '')) {
|
||||||
|
highlighterLang = 'js';
|
||||||
|
}
|
||||||
const highlightedCode = err.fullCode
|
const highlightedCode = err.fullCode
|
||||||
? highlighter.codeToHtml(err.fullCode, {
|
? highlighter.codeToHtml(err.fullCode, {
|
||||||
lang: err.loc?.file?.split('.').pop(),
|
lang: highlighterLang,
|
||||||
lineOptions: err.loc?.line ? [{ line: err.loc.line, classes: ['error-line'] }] : undefined,
|
lineOptions: err.loc?.line ? [{ line: err.loc.line, classes: ['error-line'] }] : undefined,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
AstroRenderer,
|
AstroRenderer,
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
BuildConfig,
|
BuildConfig,
|
||||||
|
ContentEntryType,
|
||||||
HookParameters,
|
HookParameters,
|
||||||
RouteData,
|
RouteData,
|
||||||
} from '../@types/astro.js';
|
} from '../@types/astro.js';
|
||||||
|
@ -15,6 +16,7 @@ import type { SerializedSSRManifest } from '../core/app/types';
|
||||||
import type { PageBuildData } from '../core/build/types';
|
import type { PageBuildData } from '../core/build/types';
|
||||||
import { mergeConfig } from '../core/config/config.js';
|
import { mergeConfig } from '../core/config/config.js';
|
||||||
import { info, LogOptions } from '../core/logger/core.js';
|
import { info, LogOptions } from '../core/logger/core.js';
|
||||||
|
import { mdxContentEntryType } from '../vite-plugin-markdown/content-entry-type.js';
|
||||||
|
|
||||||
async function withTakingALongTimeMsg<T>({
|
async function withTakingALongTimeMsg<T>({
|
||||||
name,
|
name,
|
||||||
|
@ -95,21 +97,46 @@ export async function runHookConfigSetup({
|
||||||
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
|
updatedSettings.watchFiles.push(path instanceof URL ? fileURLToPath(path) : path);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Semi-private `addPageExtension` hook
|
|
||||||
|
// ---
|
||||||
|
// Public, intentionally undocumented hooks - not subject to semver.
|
||||||
|
// Intended for internal integrations (ex. `@astrojs/mdx`),
|
||||||
|
// though accessible to integration authors if discovered.
|
||||||
|
|
||||||
function addPageExtension(...input: (string | string[])[]) {
|
function addPageExtension(...input: (string | string[])[]) {
|
||||||
const exts = (input.flat(Infinity) as string[]).map((ext) => `.${ext.replace(/^\./, '')}`);
|
const exts = (input.flat(Infinity) as string[]).map((ext) => `.${ext.replace(/^\./, '')}`);
|
||||||
updatedSettings.pageExtensions.push(...exts);
|
updatedSettings.pageExtensions.push(...exts);
|
||||||
}
|
}
|
||||||
|
function addContentEntryType(contentEntryType: ContentEntryType) {
|
||||||
|
updatedSettings.contentEntryTypes.push(contentEntryType);
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(hooks, 'addPageExtension', {
|
Object.defineProperty(hooks, 'addPageExtension', {
|
||||||
value: addPageExtension,
|
value: addPageExtension,
|
||||||
writable: false,
|
writable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(hooks, 'addContentEntryType', {
|
||||||
|
value: addContentEntryType,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
// ---
|
||||||
|
|
||||||
await withTakingALongTimeMsg({
|
await withTakingALongTimeMsg({
|
||||||
name: integration.name,
|
name: integration.name,
|
||||||
hookResult: integration.hooks['astro:config:setup'](hooks),
|
hookResult: integration.hooks['astro:config:setup'](hooks),
|
||||||
logging,
|
logging,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add MDX content entry type to support older `@astrojs/mdx` versions
|
||||||
|
// TODO: remove in next Astro minor release
|
||||||
|
if (
|
||||||
|
integration.name === '@astrojs/mdx' &&
|
||||||
|
!updatedSettings.contentEntryTypes.find((c) => c.extensions.includes('.mdx'))
|
||||||
|
) {
|
||||||
|
addContentEntryType(mdxContentEntryType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { ContentEntryType } from '../@types/astro.js';
|
||||||
|
import { parseFrontmatter } from '../content/utils.js';
|
||||||
|
|
||||||
|
export const markdownContentEntryType: ContentEntryType = {
|
||||||
|
extensions: ['.md'],
|
||||||
|
async getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||||
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||||
|
return {
|
||||||
|
data: parsed.data,
|
||||||
|
body: parsed.content,
|
||||||
|
slug: parsed.data.slug,
|
||||||
|
rawData: parsed.matter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MDX content type for compatibility with older `@astrojs/mdx` versions
|
||||||
|
* TODO: remove in next Astro minor release
|
||||||
|
*/
|
||||||
|
export const mdxContentEntryType: ContentEntryType = {
|
||||||
|
extensions: ['.mdx'],
|
||||||
|
async getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||||
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||||
|
return {
|
||||||
|
data: parsed.data,
|
||||||
|
body: parsed.content,
|
||||||
|
slug: parsed.data.slug,
|
||||||
|
rawData: parsed.matter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contentModuleTypes: `declare module 'astro:content' {
|
||||||
|
interface Render {
|
||||||
|
'.mdx': Promise<{
|
||||||
|
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||||
|
headings: import('astro').MarkdownHeading[];
|
||||||
|
remarkPluginFrontmatter: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
|
@ -3,7 +3,6 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*"
|
||||||
"@astrojs/mdx": "workspace:*"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,15 @@ const fixtures = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const contentFileExts = ['.md', '.mdx'];
|
||||||
|
|
||||||
describe('Content Collections - getEntryType', () => {
|
describe('Content Collections - getEntryType', () => {
|
||||||
fixtures.forEach(({ title, contentPaths }) => {
|
fixtures.forEach(({ title, contentPaths }) => {
|
||||||
describe(title, () => {
|
describe(title, () => {
|
||||||
it('Returns "content" for Markdown files', () => {
|
it('Returns "content" for Markdown files', () => {
|
||||||
for (const entryPath of ['blog/first-post.md', 'blog/first-post.mdx']) {
|
for (const entryPath of ['blog/first-post.md', 'blog/first-post.mdx']) {
|
||||||
const entry = fileURLToPath(new URL(entryPath, contentPaths.contentDir));
|
const entry = fileURLToPath(new URL(entryPath, contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('content');
|
expect(type).to.equal('content');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -39,44 +41,44 @@ describe('Content Collections - getEntryType', () => {
|
||||||
it('Returns "content" for Markdown files in nested directories', () => {
|
it('Returns "content" for Markdown files in nested directories', () => {
|
||||||
for (const entryPath of ['blog/2021/01/01/index.md', 'blog/2021/01/01/index.mdx']) {
|
for (const entryPath of ['blog/2021/01/01/index.md', 'blog/2021/01/01/index.mdx']) {
|
||||||
const entry = fileURLToPath(new URL(entryPath, contentPaths.contentDir));
|
const entry = fileURLToPath(new URL(entryPath, contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('content');
|
expect(type).to.equal('content');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "config" for config files', () => {
|
it('Returns "config" for config files', () => {
|
||||||
const entry = fileURLToPath(contentPaths.config.url);
|
const entry = fileURLToPath(contentPaths.config.url);
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('config');
|
expect(type).to.equal('config');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "unsupported" for non-Markdown files', () => {
|
it('Returns "unsupported" for non-Markdown files', () => {
|
||||||
const entry = fileURLToPath(new URL('blog/robots.txt', contentPaths.contentDir));
|
const entry = fileURLToPath(new URL('blog/robots.txt', contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('unsupported');
|
expect(type).to.equal('unsupported');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "ignored" for .DS_Store', () => {
|
it('Returns "ignored" for .DS_Store', () => {
|
||||||
const entry = fileURLToPath(new URL('blog/.DS_Store', contentPaths.contentDir));
|
const entry = fileURLToPath(new URL('blog/.DS_Store', contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('ignored');
|
expect(type).to.equal('ignored');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "ignored" for unsupported files using an underscore', () => {
|
it('Returns "ignored" for unsupported files using an underscore', () => {
|
||||||
const entry = fileURLToPath(new URL('blog/_draft-robots.txt', contentPaths.contentDir));
|
const entry = fileURLToPath(new URL('blog/_draft-robots.txt', contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('ignored');
|
expect(type).to.equal('ignored');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "ignored" when using underscore on file name', () => {
|
it('Returns "ignored" when using underscore on file name', () => {
|
||||||
const entry = fileURLToPath(new URL('blog/_first-post.md', contentPaths.contentDir));
|
const entry = fileURLToPath(new URL('blog/_first-post.md', contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('ignored');
|
expect(type).to.equal('ignored');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns "ignored" when using underscore on directory name', () => {
|
it('Returns "ignored" when using underscore on directory name', () => {
|
||||||
const entry = fileURLToPath(new URL('blog/_draft/first-post.md', contentPaths.contentDir));
|
const entry = fileURLToPath(new URL('blog/_draft/first-post.md', contentPaths.contentDir));
|
||||||
const type = getEntryType(entry, contentPaths);
|
const type = getEntryType(entry, contentPaths, contentFileExts);
|
||||||
expect(type).to.equal('ignored');
|
expect(type).to.equal('ignored');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
348
packages/integrations/markdoc/README.md
Normal file
348
packages/integrations/markdoc/README.md
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
# @astrojs/markdoc (experimental) 📝
|
||||||
|
|
||||||
|
This **[Astro integration][astro-integration]** enables the usage of [Markdoc](https://markdoc.dev/) to create components, pages, and content collection entries.
|
||||||
|
|
||||||
|
- <strong>[Why Markdoc?](#why-markdoc)</strong>
|
||||||
|
- <strong>[Installation](#installation)</strong>
|
||||||
|
- <strong>[Usage](#usage)</strong>
|
||||||
|
- <strong>[Configuration](#configuration)</strong>
|
||||||
|
- <strong>[Examples](#examples)</strong>
|
||||||
|
- <strong>[Troubleshooting](#troubleshooting)</strong>
|
||||||
|
- <strong>[Contributing](#contributing)</strong>
|
||||||
|
- <strong>[Changelog](#changelog)</strong>
|
||||||
|
|
||||||
|
## Why Markdoc?
|
||||||
|
|
||||||
|
Markdoc allows you to enhance your Markdown with [Astro components][astro-components]. If you have existing content authored in Markdoc, this integration allows you to bring those files to your Astro project using content collections.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Quick Install
|
||||||
|
|
||||||
|
The `astro add` command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren't sure which package manager you're using, run the first command.) Then, follow the prompts, and type "y" in the terminal (meaning "yes") for each one.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using NPM
|
||||||
|
npx astro add markdoc
|
||||||
|
# Using Yarn
|
||||||
|
yarn astro add markdoc
|
||||||
|
# Using PNPM
|
||||||
|
pnpm astro add markdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run into any issues, [feel free to report them to us on GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below.
|
||||||
|
|
||||||
|
### Manual Install
|
||||||
|
|
||||||
|
First, install the `@astrojs/markdoc` package using your package manager. If you're using npm or aren't sure, run this in the terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install @astrojs/markdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, apply this integration to your `astro.config.*` file using the `integrations` property:
|
||||||
|
|
||||||
|
__`astro.config.mjs`__
|
||||||
|
|
||||||
|
```js ins={2} "markdoc()"
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// ...
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Editor Integration
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) supports Markdown by default. However, for Markdoc editor support, you may wish to add the following setting in your VSCode config. This ensures authoring Markdoc files provides a Markdown-like editor experience.
|
||||||
|
|
||||||
|
```json title=".vscode/settings.json"
|
||||||
|
"files.associations": {
|
||||||
|
"*.mdoc": "markdown"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Markdoc files can only be used within content collections. Add entries to any content collection using the `.mdoc` extension:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
src/content/docs/
|
||||||
|
why-markdoc.mdoc
|
||||||
|
quick-start.mdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, query your collection using the [Content Collection APIs](https://docs.astro.build/en/guides/content-collections/#querying-collections):
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
|
||||||
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--Access frontmatter properties with `data`-->
|
||||||
|
<h1>{entry.data.title}</h1>
|
||||||
|
<!--Render Markdoc contents with the Content component-->
|
||||||
|
<Content />
|
||||||
|
```
|
||||||
|
|
||||||
|
📚 See the [Astro Content Collection docs][astro-content-collections] for more information.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`@astrojs/markdoc` offers configuration options to use all of Markdoc's features and connect UI components to your content.
|
||||||
|
|
||||||
|
### Using components
|
||||||
|
|
||||||
|
You can add Astro and UI framework components (React, Vue, Svelte, etc.) to your Markdoc using both [Markdoc tags][markdoc-tags] and HTML element [nodes][markdoc-nodes].
|
||||||
|
|
||||||
|
#### Render Markdoc tags as Astro components
|
||||||
|
|
||||||
|
You may configure [Markdoc tags][markdoc-tags] that map to components. You can configure a new tag from your `astro.config` using the `tags` attribute.
|
||||||
|
|
||||||
|
|
||||||
|
```js
|
||||||
|
// astro.config.mjs
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
tags: {
|
||||||
|
aside: {
|
||||||
|
render: 'Aside',
|
||||||
|
attributes: {
|
||||||
|
// Component props as attribute definitions
|
||||||
|
// See Markdoc's documentation on defining attributes
|
||||||
|
// https://markdoc.dev/docs/attributes#defining-attributes
|
||||||
|
type: { type: String },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can wire this render name (`'Aside'`) to a component from the `components` prop via the `<Content />` component. Note the object key name (`Aside` in this case) should match the render name:
|
||||||
|
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
import Aside from '../components/Aside.astro';
|
||||||
|
|
||||||
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content
|
||||||
|
components={{ Aside }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Render Markdoc nodes / HTML elements as Astro components
|
||||||
|
|
||||||
|
You may also want to map standard HTML elements like headings and paragraphs to components. For this, you can configure a custom [Markdoc node][markdoc-nodes]. This example overrides Markdoc's `heading` node to render a `Heading` component, passing the built-in `level` attribute as a prop:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// astro.config.mjs
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
nodes: {
|
||||||
|
heading: {
|
||||||
|
render: 'Heading',
|
||||||
|
// Markdoc requires type defs for each attribute.
|
||||||
|
// These should mirror the `Props` type of the component
|
||||||
|
// you are rendering.
|
||||||
|
// See Markdoc's documentation on defining attributes
|
||||||
|
// https://markdoc.dev/docs/attributes#defining-attributes
|
||||||
|
attributes: {
|
||||||
|
level: { type: String },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you can map the string passed to render (`'Heading'` in this example) to a component import. This is configured from the `<Content />` component used to render your Markdoc using the `components` prop:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
import Heading from '../components/Heading.astro';
|
||||||
|
|
||||||
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content
|
||||||
|
components={{ Heading }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, all Markdown headings will render with the `Heading.astro` component. This example uses a level 3 heading, automatically passing `level: 3` as the component prop:
|
||||||
|
|
||||||
|
```md
|
||||||
|
### I'm a level 3 heading!
|
||||||
|
```
|
||||||
|
|
||||||
|
📚 [Find all of Markdoc's built-in nodes and node attributes on their documentation.](https://markdoc.dev/docs/nodes#built-in-nodes)
|
||||||
|
|
||||||
|
#### Use client-side UI components
|
||||||
|
|
||||||
|
Today, the `components` prop does not support the `client:` directive for hydrating components. To embed client-side components, create a wrapper `.astro` file to import your component and apply a `client:` directive manually.
|
||||||
|
|
||||||
|
This example wraps a `Aside.tsx` component with a `ClientAside.astro` wrapper:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// src/components/ClientAside.astro
|
||||||
|
import Aside from './Aside';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Aside client:load />
|
||||||
|
```
|
||||||
|
|
||||||
|
This component [can be applied via the `components` prop](#render-html-elements-as-astro-components):
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// src/pages/why-markdoc.astro
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
import ClientAside from '../components/ClientAside.astro';
|
||||||
|
|
||||||
|
const entry = await getEntryBySlug('docs', 'why-markdoc');
|
||||||
|
const { Content } = await entry.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<Content
|
||||||
|
components={{
|
||||||
|
Aside: ClientAside,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Markdoc config
|
||||||
|
|
||||||
|
The Markdoc integration accepts [all Markdoc configuration options](https://markdoc.dev/docs/config), including [tags](https://markdoc.dev/docs/tags) and [functions](https://markdoc.dev/docs/functions).
|
||||||
|
|
||||||
|
You can pass these options from the `markdoc()` integration in your `astro.config`. This example adds a global `getCountryEmoji` function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// astro.config.mjs
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
functions: {
|
||||||
|
getCountryEmoji: {
|
||||||
|
transform(parameters) {
|
||||||
|
const [country] = Object.values(parameters);
|
||||||
|
const countryToEmojiMap = {
|
||||||
|
japan: '🇯🇵',
|
||||||
|
spain: '🇪🇸',
|
||||||
|
france: '🇫🇷',
|
||||||
|
}
|
||||||
|
return countryToEmojiMap[country] ?? '🏳'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you can call this function from any Markdoc content entry:
|
||||||
|
|
||||||
|
```md
|
||||||
|
¡Hola {% getCountryEmoji("spain") %}!
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
These options will be applied during [the Markdoc "transform" phase](https://markdoc.dev/docs/render#transform). This is run **at build time** (rather than server request time) both for static and SSR Astro projects. If you need to define configuration at runtime (ex. SSR variables), [see the next section](#Define-markdoc-configuration-at-runtime).
|
||||||
|
:::
|
||||||
|
|
||||||
|
📚 [See the Markdoc documentation](https://markdoc.dev/docs/functions#creating-a-custom-function) for more on using variables or functions in your content.
|
||||||
|
|
||||||
|
### Define Markdoc configuration at runtime
|
||||||
|
|
||||||
|
You may need to define Markdoc configuration at the component level, rather than the `astro.config.mjs` level. This is useful when mapping props and SSR parameters to [Markdoc variables](https://markdoc.dev/docs/variables).
|
||||||
|
|
||||||
|
Astro recommends running the Markdoc transform step manually. This allows you to define your configuration and call Markdoc's rendering functions in a `.astro` file directly, ignoring any Markdoc config in your `astro.config.mjs`.
|
||||||
|
|
||||||
|
You will need to install the `@markdoc/markdoc` package into your project first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using NPM
|
||||||
|
npx astro add @markdoc/markdoc
|
||||||
|
# Using Yarn
|
||||||
|
yarn astro add @markdoc/markdoc
|
||||||
|
# Using PNPM
|
||||||
|
pnpm astro add @markdoc/markdoc
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you can define Markdoc configuration options using `Markdock.transform()`.
|
||||||
|
|
||||||
|
This example defines an `abTestGroup` Markdoc variable based on an SSR param, transforming the raw entry `body`. The result is rendered using the `Renderer` component provided by `@astrojs/markdoc`:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import Markdoc from '@markdoc/markdoc';
|
||||||
|
import { Renderer } from '@astrojs/markdoc/components';
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
|
||||||
|
const { body } = await getEntryBySlug('docs', 'with-ab-test');
|
||||||
|
const ast = Markdoc.parse(body);
|
||||||
|
const content = Markdoc.transform({
|
||||||
|
variables: { abTestGroup: Astro.params.abTestGroup },
|
||||||
|
}, ast);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Renderer {content} components={{ /* same `components` prop used by the `Content` component */ }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
* The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
|
||||||
|
|
||||||
|
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR!
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](https://github.com/withastro/astro/tree/main/packages/integrations/markdoc/CHANGELOG.md) for a history of changes to this integration.
|
||||||
|
|
||||||
|
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
|
||||||
|
|
||||||
|
[astro-components]: https://docs.astro.build/en/core-concepts/astro-components/
|
||||||
|
|
||||||
|
[astro-content-collections]: https://docs.astro.build/en/guides/content-collections/
|
||||||
|
|
||||||
|
[markdoc-tags]: https://markdoc.dev/docs/tags
|
||||||
|
|
||||||
|
[markdoc-nodes]: https://markdoc.dev/docs/nodes
|
30
packages/integrations/markdoc/components/RenderNode.astro
Normal file
30
packages/integrations/markdoc/components/RenderNode.astro
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
import stringifyAttributes from 'stringify-attributes';
|
||||||
|
import type { AstroNode } from './astroNode';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
node: AstroNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Node = (Astro.props as Props).node;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
typeof Node === 'string' ? (
|
||||||
|
<Fragment set:text={Node} />
|
||||||
|
) : 'component' in Node ? (
|
||||||
|
<Node.component {...Node.props}>
|
||||||
|
{Node.children.map((child) => (
|
||||||
|
<Astro.self node={child} />
|
||||||
|
))}
|
||||||
|
</Node.component>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
<Fragment set:html={`<${Node.tag} ${stringifyAttributes(Node.attributes)}>`} />
|
||||||
|
{Node.children.map((child) => (
|
||||||
|
<Astro.self node={child} />
|
||||||
|
))}
|
||||||
|
<Fragment set:html={`</${Node.tag}>`} />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
21
packages/integrations/markdoc/components/Renderer.astro
Normal file
21
packages/integrations/markdoc/components/Renderer.astro
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
||||||
|
import type { AstroInstance } from 'astro';
|
||||||
|
import { validateComponentsProp } from '../dist/utils.js';
|
||||||
|
import { createAstroNode } from './astroNode';
|
||||||
|
import RenderNode from './RenderNode.astro';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
content: RenderableTreeNode;
|
||||||
|
components?: Record<string, AstroInstance['default']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { content, components } = Astro.props as Props;
|
||||||
|
|
||||||
|
// Will throw if components is invalid
|
||||||
|
if (components) {
|
||||||
|
validateComponentsProp(components);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<RenderNode node={createAstroNode(content, components)} />
|
51
packages/integrations/markdoc/components/astroNode.ts
Normal file
51
packages/integrations/markdoc/components/astroNode.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type { AstroInstance } from 'astro';
|
||||||
|
import type { RenderableTreeNode } from '@markdoc/markdoc';
|
||||||
|
import Markdoc from '@markdoc/markdoc';
|
||||||
|
import { MarkdocError, isCapitalized } from '../dist/utils.js';
|
||||||
|
|
||||||
|
export type AstroNode =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
component: AstroInstance['default'];
|
||||||
|
props: Record<string, any>;
|
||||||
|
children: AstroNode[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
tag: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
children: AstroNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createAstroNode(
|
||||||
|
node: RenderableTreeNode,
|
||||||
|
components: Record<string, AstroInstance['default']> = {}
|
||||||
|
): AstroNode {
|
||||||
|
if (typeof node === 'string' || typeof node === 'number') {
|
||||||
|
return String(node);
|
||||||
|
} else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name in components) {
|
||||||
|
const component = components[node.name];
|
||||||
|
const props = node.attributes;
|
||||||
|
const children = node.children.map((child) => createAstroNode(child, components));
|
||||||
|
|
||||||
|
return {
|
||||||
|
component,
|
||||||
|
props,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
} else if (isCapitalized(node.name)) {
|
||||||
|
throw new MarkdocError({
|
||||||
|
message: `Unable to render ${JSON.stringify(node.name)}.`,
|
||||||
|
hint: 'Did you add this to the "components" prop on your <Content /> component?',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
tag: node.name,
|
||||||
|
attributes: node.attributes,
|
||||||
|
children: node.children.map((child) => createAstroNode(child, components)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
2
packages/integrations/markdoc/components/index.ts
Normal file
2
packages/integrations/markdoc/components/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// @ts-expect-error
|
||||||
|
export { default as Renderer } from './Renderer.astro';
|
54
packages/integrations/markdoc/package.json
Normal file
54
packages/integrations/markdoc/package.json
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "@astrojs/markdoc",
|
||||||
|
"description": "Use Markdoc within Astro",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"author": "withastro",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/withastro/astro.git",
|
||||||
|
"directory": "packages/integrations/markdoc"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"astro-integration",
|
||||||
|
"astro-component",
|
||||||
|
"markdoc"
|
||||||
|
],
|
||||||
|
"bugs": "https://github.com/withastro/astro/issues",
|
||||||
|
"homepage": "https://docs.astro.build/en/guides/integrations-guide/markdoc/",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./components": "./components/index.ts",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
|
||||||
|
"build:ci": "astro-scripts build \"src/**/*.ts\"",
|
||||||
|
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||||
|
"test": "mocha --exit --timeout 20000",
|
||||||
|
"test:match": "mocha --timeout 20000 -g"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@markdoc/markdoc": "^0.2.2",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"stringify-attributes": "^3.0.0",
|
||||||
|
"zod": "^3.17.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.1",
|
||||||
|
"@types/html-escaper": "^3.0.0",
|
||||||
|
"@types/mocha": "^9.1.1",
|
||||||
|
"astro": "workspace:*",
|
||||||
|
"astro-scripts": "workspace:*",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"devalue": "^4.2.0",
|
||||||
|
"linkedom": "^0.14.12",
|
||||||
|
"mocha": "^9.2.2",
|
||||||
|
"vite": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.12.0"
|
||||||
|
}
|
||||||
|
}
|
127
packages/integrations/markdoc/src/index.ts
Normal file
127
packages/integrations/markdoc/src/index.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import type { AstroIntegration, AstroConfig, ContentEntryType, HookParameters } from 'astro';
|
||||||
|
import { InlineConfig } from 'vite';
|
||||||
|
import type { Config } from '@markdoc/markdoc';
|
||||||
|
import Markdoc from '@markdoc/markdoc';
|
||||||
|
import {
|
||||||
|
prependForwardSlash,
|
||||||
|
getAstroConfigPath,
|
||||||
|
MarkdocError,
|
||||||
|
parseFrontmatter,
|
||||||
|
} from './utils.js';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
type IntegrationWithPrivateHooks = {
|
||||||
|
name: string;
|
||||||
|
hooks: Omit<AstroIntegration['hooks'], 'astro:config:setup'> & {
|
||||||
|
'astro:config:setup': (
|
||||||
|
params: HookParameters<'astro:config:setup'> & {
|
||||||
|
// `contentEntryType` is not a public API
|
||||||
|
// Add type defs here
|
||||||
|
addContentEntryType: (contentEntryType: ContentEntryType) => void;
|
||||||
|
}
|
||||||
|
) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function markdoc(markdocConfig: Config = {}): IntegrationWithPrivateHooks {
|
||||||
|
return {
|
||||||
|
name: '@astrojs/markdoc',
|
||||||
|
hooks: {
|
||||||
|
'astro:config:setup': async ({ updateConfig, config, addContentEntryType }) => {
|
||||||
|
function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||||
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||||
|
return {
|
||||||
|
data: parsed.data,
|
||||||
|
body: parsed.content,
|
||||||
|
slug: parsed.data.slug,
|
||||||
|
rawData: parsed.matter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
addContentEntryType({
|
||||||
|
extensions: ['.mdoc'],
|
||||||
|
getEntryInfo,
|
||||||
|
contentModuleTypes: await fs.promises.readFile(
|
||||||
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
||||||
|
'utf-8'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const viteConfig: InlineConfig = {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: '@astrojs/markdoc',
|
||||||
|
async transform(code, id) {
|
||||||
|
if (!id.endsWith('.mdoc')) return;
|
||||||
|
|
||||||
|
validateRenderProperties(markdocConfig, config);
|
||||||
|
const body = getEntryInfo({
|
||||||
|
// Can't use `pathToFileUrl` - Vite IDs are not plain file paths
|
||||||
|
fileUrl: new URL(prependForwardSlash(id), 'file://'),
|
||||||
|
contents: code,
|
||||||
|
}).body;
|
||||||
|
const ast = Markdoc.parse(body);
|
||||||
|
const content = Markdoc.transform(ast, markdocConfig);
|
||||||
|
|
||||||
|
return `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify(
|
||||||
|
content
|
||||||
|
)};\nexport async function Content ({ components }) { return h(Renderer, { content: transformedContent, components }); }\nContent[Symbol.for('astro.needsHeadRendering')] = true;`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
updateConfig({ vite: viteConfig });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRenderProperties(markdocConfig: Config, astroConfig: AstroConfig) {
|
||||||
|
const tags = markdocConfig.tags ?? {};
|
||||||
|
const nodes = markdocConfig.nodes ?? {};
|
||||||
|
|
||||||
|
for (const [name, config] of Object.entries(tags)) {
|
||||||
|
validateRenderProperty({ type: 'tag', name, config, astroConfig });
|
||||||
|
}
|
||||||
|
for (const [name, config] of Object.entries(nodes)) {
|
||||||
|
validateRenderProperty({ type: 'node', name, config, astroConfig });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRenderProperty({
|
||||||
|
name,
|
||||||
|
config,
|
||||||
|
type,
|
||||||
|
astroConfig,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
config: { render?: string };
|
||||||
|
type: 'node' | 'tag';
|
||||||
|
astroConfig: Pick<AstroConfig, 'root'>;
|
||||||
|
}) {
|
||||||
|
if (typeof config.render === 'string' && config.render.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid ${type} configuration: ${JSON.stringify(
|
||||||
|
name
|
||||||
|
)}. The "render" property cannot be an empty string.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof config.render === 'string' && !isCapitalized(config.render)) {
|
||||||
|
const astroConfigPath = getAstroConfigPath(fs, fileURLToPath(astroConfig.root));
|
||||||
|
throw new MarkdocError({
|
||||||
|
message: `Invalid ${type} configuration: ${JSON.stringify(
|
||||||
|
name
|
||||||
|
)}. The "render" property must reference a capitalized component name.`,
|
||||||
|
hint: 'If you want to render to an HTML element, see our docs on rendering Markdoc manually: https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components',
|
||||||
|
location: astroConfigPath
|
||||||
|
? {
|
||||||
|
file: astroConfigPath,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCapitalized(str: string) {
|
||||||
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
||||||
|
}
|
147
packages/integrations/markdoc/src/utils.ts
Normal file
147
packages/integrations/markdoc/src/utils.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import path from 'node:path';
|
||||||
|
import type fsMod from 'node:fs';
|
||||||
|
import type { ErrorPayload as ViteErrorPayload } from 'vite';
|
||||||
|
import type { AstroInstance } from 'astro';
|
||||||
|
import z from 'astro/zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match YAML exception handling from Astro core errors
|
||||||
|
* @see 'astro/src/core/errors.ts'
|
||||||
|
*/
|
||||||
|
export function parseFrontmatter(fileContents: string, filePath: string) {
|
||||||
|
try {
|
||||||
|
// `matter` is empty string on cache results
|
||||||
|
// clear cache to prevent this
|
||||||
|
(matter as any).clearCache();
|
||||||
|
return matter(fileContents);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.name === 'YAMLException') {
|
||||||
|
const err: Error & ViteErrorPayload['err'] = e;
|
||||||
|
err.id = filePath;
|
||||||
|
err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
|
||||||
|
err.message = e.reason;
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches AstroError object with types like error codes stubbed out
|
||||||
|
* @see 'astro/src/core/errors/errors.ts'
|
||||||
|
*/
|
||||||
|
export class MarkdocError extends Error {
|
||||||
|
public errorCode: number;
|
||||||
|
public loc: ErrorLocation | undefined;
|
||||||
|
public title: string | undefined;
|
||||||
|
public hint: string | undefined;
|
||||||
|
public frame: string | undefined;
|
||||||
|
|
||||||
|
type = 'MarkdocError';
|
||||||
|
|
||||||
|
constructor(props: ErrorProperties, ...params: any) {
|
||||||
|
super(...params);
|
||||||
|
|
||||||
|
const {
|
||||||
|
// Use default code for unknown errors in Astro core
|
||||||
|
// We don't have a best practice for integration error codes yet
|
||||||
|
code = 99999,
|
||||||
|
name,
|
||||||
|
title = 'MarkdocError',
|
||||||
|
message,
|
||||||
|
stack,
|
||||||
|
location,
|
||||||
|
hint,
|
||||||
|
frame,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this.errorCode = code;
|
||||||
|
this.title = title;
|
||||||
|
if (message) this.message = message;
|
||||||
|
// Only set this if we actually have a stack passed, otherwise uses Error's
|
||||||
|
this.stack = stack ? stack : this.stack;
|
||||||
|
this.loc = location;
|
||||||
|
this.hint = hint;
|
||||||
|
this.frame = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorLocation {
|
||||||
|
file?: string;
|
||||||
|
line?: number;
|
||||||
|
column?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorProperties {
|
||||||
|
code?: number;
|
||||||
|
title?: string;
|
||||||
|
name?: string;
|
||||||
|
message?: string;
|
||||||
|
location?: ErrorLocation;
|
||||||
|
hint?: string;
|
||||||
|
stack?: string;
|
||||||
|
frame?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches `search` function used for resolving `astro.config` files.
|
||||||
|
* Used by Markdoc for error handling.
|
||||||
|
* @see 'astro/src/core/config/config.ts'
|
||||||
|
*/
|
||||||
|
export function getAstroConfigPath(fs: typeof fsMod, root: string): string | undefined {
|
||||||
|
const paths = [
|
||||||
|
'astro.config.mjs',
|
||||||
|
'astro.config.js',
|
||||||
|
'astro.config.ts',
|
||||||
|
'astro.config.mts',
|
||||||
|
'astro.config.cjs',
|
||||||
|
'astro.config.cts',
|
||||||
|
].map((p) => path.join(root, p));
|
||||||
|
|
||||||
|
for (const file of paths) {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see 'astro/src/core/path.ts'
|
||||||
|
*/
|
||||||
|
export function prependForwardSlash(str: string) {
|
||||||
|
return str[0] === '/' ? str : '/' + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateComponentsProp(components: Record<string, AstroInstance['default']>) {
|
||||||
|
try {
|
||||||
|
componentsPropValidator.parse(components);
|
||||||
|
} catch (e) {
|
||||||
|
throw new MarkdocError({
|
||||||
|
message:
|
||||||
|
e instanceof z.ZodError
|
||||||
|
? e.issues[0].message
|
||||||
|
: 'Invalid `components` prop. Ensure you are passing an object of components to <Content />',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentsPropValidator = z.record(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Invalid `components` prop. Component names cannot be empty!')
|
||||||
|
.refine(
|
||||||
|
(value) => isCapitalized(value),
|
||||||
|
(value) => ({
|
||||||
|
message: `Invalid \`components\` prop: ${JSON.stringify(
|
||||||
|
value
|
||||||
|
)}. Component name must be capitalized. If you want to render HTML elements as components, try using a Markdoc node (https://docs.astro.build/en/guides/integrations-guide/markdoc/#render-markdoc-nodes--html-elements-as-astro-components)`,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
z.any()
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isCapitalized(str: string) {
|
||||||
|
return str.length > 0 && str[0] === str[0].toUpperCase();
|
||||||
|
}
|
9
packages/integrations/markdoc/template/content-module-types.d.ts
vendored
Normal file
9
packages/integrations/markdoc/template/content-module-types.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
declare module 'astro:content' {
|
||||||
|
interface Render {
|
||||||
|
'.mdoc': Promise<{
|
||||||
|
Content(props: {
|
||||||
|
components?: Record<string, import('astro').AstroInstance['default']>;
|
||||||
|
}): import('astro').MarkdownInstance<{}>['Content'];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}
|
258
packages/integrations/markdoc/test/content-collections.test.js
Normal file
258
packages/integrations/markdoc/test/content-collections.test.js
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
import { parseHTML } from 'linkedom';
|
||||||
|
import { parse as parseDevalue } from 'devalue';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { loadFixture, fixLineEndings } from '../../../astro/test/test-utils.js';
|
||||||
|
import markdoc from '../dist/index.js';
|
||||||
|
|
||||||
|
function formatPost(post) {
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
body: fixLineEndings(post.body),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = new URL('./fixtures/content-collections/', import.meta.url);
|
||||||
|
|
||||||
|
describe('Markdoc - Content Collections', () => {
|
||||||
|
let baseFixture;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
baseFixture = await loadFixture({
|
||||||
|
root,
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dev', () => {
|
||||||
|
let devServer;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
devServer = await baseFixture.startDevServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await devServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads entry', async () => {
|
||||||
|
const res = await baseFixture.fetch('/entry.json');
|
||||||
|
const post = parseDevalue(await res.text());
|
||||||
|
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads collection', async () => {
|
||||||
|
const res = await baseFixture.fetch('/collection.json');
|
||||||
|
const posts = parseDevalue(await res.text());
|
||||||
|
expect(posts).to.not.be.null;
|
||||||
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||||
|
simplePostEntry,
|
||||||
|
withComponentsEntry,
|
||||||
|
withConfigEntry,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - simple', async () => {
|
||||||
|
const res = await baseFixture.fetch('/content-simple');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Simple post');
|
||||||
|
const p = document.querySelector('p');
|
||||||
|
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with config', async () => {
|
||||||
|
const fixture = await getFixtureWithConfig();
|
||||||
|
const server = await fixture.startDevServer();
|
||||||
|
|
||||||
|
const res = await fixture.fetch('/content-with-config');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with config');
|
||||||
|
const textContent = html;
|
||||||
|
|
||||||
|
expect(textContent).to.not.include('Hello');
|
||||||
|
expect(textContent).to.include('Hola');
|
||||||
|
expect(textContent).to.include(`Konnichiwa`);
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with components', async () => {
|
||||||
|
const fixture = await getFixtureWithComponents();
|
||||||
|
const server = await fixture.startDevServer();
|
||||||
|
|
||||||
|
const res = await fixture.fetch('/content-with-components');
|
||||||
|
const html = await res.text();
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with components');
|
||||||
|
|
||||||
|
// Renders custom shortcode component
|
||||||
|
const marquee = document.querySelector('marquee');
|
||||||
|
expect(marquee).to.not.be.null;
|
||||||
|
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
||||||
|
|
||||||
|
// Renders Astro Code component
|
||||||
|
const pre = document.querySelector('pre');
|
||||||
|
expect(pre).to.not.be.null;
|
||||||
|
expect(pre.className).to.equal('astro-code');
|
||||||
|
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('build', () => {
|
||||||
|
before(async () => {
|
||||||
|
await baseFixture.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads entry', async () => {
|
||||||
|
const res = await baseFixture.readFile('/entry.json');
|
||||||
|
const post = parseDevalue(res);
|
||||||
|
expect(formatPost(post)).to.deep.equal(simplePostEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads collection', async () => {
|
||||||
|
const res = await baseFixture.readFile('/collection.json');
|
||||||
|
const posts = parseDevalue(res);
|
||||||
|
expect(posts).to.not.be.null;
|
||||||
|
expect(posts.sort().map((post) => formatPost(post))).to.deep.equal([
|
||||||
|
simplePostEntry,
|
||||||
|
withComponentsEntry,
|
||||||
|
withConfigEntry,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - simple', async () => {
|
||||||
|
const html = await baseFixture.readFile('/content-simple/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Simple post');
|
||||||
|
const p = document.querySelector('p');
|
||||||
|
expect(p.textContent).to.equal('This is a simple Markdoc post.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with config', async () => {
|
||||||
|
const fixture = await getFixtureWithConfig();
|
||||||
|
await fixture.build();
|
||||||
|
|
||||||
|
const html = await fixture.readFile('/content-with-config/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with config');
|
||||||
|
const textContent = html;
|
||||||
|
|
||||||
|
expect(textContent).to.not.include('Hello');
|
||||||
|
expect(textContent).to.include('Hola');
|
||||||
|
expect(textContent).to.include(`Konnichiwa`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders content - with components', async () => {
|
||||||
|
const fixture = await getFixtureWithComponents();
|
||||||
|
await fixture.build();
|
||||||
|
|
||||||
|
const html = await fixture.readFile('/content-with-components/index.html');
|
||||||
|
const { document } = parseHTML(html);
|
||||||
|
const h2 = document.querySelector('h2');
|
||||||
|
expect(h2.textContent).to.equal('Post with components');
|
||||||
|
|
||||||
|
// Renders custom shortcode component
|
||||||
|
const marquee = document.querySelector('marquee');
|
||||||
|
expect(marquee).to.not.be.null;
|
||||||
|
expect(marquee.hasAttribute('data-custom-marquee')).to.equal(true);
|
||||||
|
|
||||||
|
// Renders Astro Code component
|
||||||
|
const pre = document.querySelector('pre');
|
||||||
|
expect(pre).to.not.be.null;
|
||||||
|
expect(pre.className).to.equal('astro-code');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFixtureWithConfig() {
|
||||||
|
return loadFixture({
|
||||||
|
root,
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
variables: {
|
||||||
|
countries: ['ES', 'JP'],
|
||||||
|
},
|
||||||
|
functions: {
|
||||||
|
includes: {
|
||||||
|
transform(parameters) {
|
||||||
|
const [array, value] = Object.values(parameters);
|
||||||
|
return Array.isArray(array) ? array.includes(value) : false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFixtureWithComponents() {
|
||||||
|
return loadFixture({
|
||||||
|
root,
|
||||||
|
integrations: [
|
||||||
|
markdoc({
|
||||||
|
nodes: {
|
||||||
|
fence: {
|
||||||
|
render: 'Code',
|
||||||
|
attributes: {
|
||||||
|
language: { type: String },
|
||||||
|
content: { type: String },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
mq: {
|
||||||
|
render: 'CustomMarquee',
|
||||||
|
attributes: {
|
||||||
|
direction: {
|
||||||
|
type: String,
|
||||||
|
default: 'left',
|
||||||
|
matches: ['left', 'right', 'up', 'down'],
|
||||||
|
errorLevel: 'critical',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const simplePostEntry = {
|
||||||
|
id: 'simple.mdoc',
|
||||||
|
slug: 'simple',
|
||||||
|
collection: 'blog',
|
||||||
|
data: {
|
||||||
|
schemaWorks: true,
|
||||||
|
title: 'Simple post',
|
||||||
|
},
|
||||||
|
body: '\n## Simple post\n\nThis is a simple Markdoc post.\n',
|
||||||
|
};
|
||||||
|
|
||||||
|
const withComponentsEntry = {
|
||||||
|
id: 'with-components.mdoc',
|
||||||
|
slug: 'with-components',
|
||||||
|
collection: 'blog',
|
||||||
|
data: {
|
||||||
|
schemaWorks: true,
|
||||||
|
title: 'Post with components',
|
||||||
|
},
|
||||||
|
body: '\n## Post with components\n\nThis uses a custom marquee component with a shortcode:\n\n{% mq direction="right" %}\nI\'m a marquee too!\n{% /mq %}\n\nAnd a code component for code blocks:\n\n```js\nconst isRenderedWithShiki = true;\n```\n',
|
||||||
|
};
|
||||||
|
|
||||||
|
const withConfigEntry = {
|
||||||
|
id: 'with-config.mdoc',
|
||||||
|
slug: 'with-config',
|
||||||
|
collection: 'blog',
|
||||||
|
data: {
|
||||||
|
schemaWorks: true,
|
||||||
|
title: 'Post with config',
|
||||||
|
},
|
||||||
|
body: '\n## Post with config\n\n{% if includes($countries, "EN") %} Hello {% /if %}\n{% if includes($countries, "ES") %} Hola {% /if %}\n{% if includes($countries, "JP") %} Konnichiwa {% /if %}\n',
|
||||||
|
};
|
7
packages/integrations/markdoc/test/fixtures/content-collections/astro.config.mjs
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/content-collections/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import markdoc from '@astrojs/markdoc';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [markdoc()],
|
||||||
|
});
|
13
packages/integrations/markdoc/test/fixtures/content-collections/package.json
vendored
Normal file
13
packages/integrations/markdoc/test/fixtures/content-collections/package.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "@test/markdoc-content-collections",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/markdoc": "workspace:*",
|
||||||
|
"@markdoc/markdoc": "^0.2.2",
|
||||||
|
"astro": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shiki": "^0.11.1"
|
||||||
|
}
|
||||||
|
}
|
12
packages/integrations/markdoc/test/fixtures/content-collections/src/components/Code.astro
vendored
Normal file
12
packages/integrations/markdoc/test/fixtures/content-collections/src/components/Code.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import { Code } from 'astro/components';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { content, language } = Astro.props as Props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Code lang={language} code={content} />
|
|
@ -0,0 +1 @@
|
||||||
|
<marquee data-custom-marquee {...Astro.props}><slot /></marquee>
|
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/simple.mdoc
vendored
Normal file
7
packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/simple.mdoc
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
title: Simple post
|
||||||
|
---
|
||||||
|
|
||||||
|
## Simple post
|
||||||
|
|
||||||
|
This is a simple Markdoc post.
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: Post with components
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post with components
|
||||||
|
|
||||||
|
This uses a custom marquee component with a shortcode:
|
||||||
|
|
||||||
|
{% mq direction="right" %}
|
||||||
|
I'm a marquee too!
|
||||||
|
{% /mq %}
|
||||||
|
|
||||||
|
And a code component for code blocks:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isRenderedWithShiki = true;
|
||||||
|
```
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
title: Post with config
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post with config
|
||||||
|
|
||||||
|
{% if includes($countries, "EN") %} Hello {% /if %}
|
||||||
|
{% if includes($countries, "ES") %} Hola {% /if %}
|
||||||
|
{% if includes($countries, "JP") %} Konnichiwa {% /if %}
|
12
packages/integrations/markdoc/test/fixtures/content-collections/src/content/config.ts
vendored
Normal file
12
packages/integrations/markdoc/test/fixtures/content-collections/src/content/config.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
}).transform(data => ({
|
||||||
|
...data,
|
||||||
|
schemaWorks: true,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog };
|
10
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/collection.json.js
vendored
Normal file
10
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/collection.json.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { stringify } from 'devalue';
|
||||||
|
import { stripAllRenderFn } from '../../utils.js';
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
const posts = await getCollection('blog');
|
||||||
|
return {
|
||||||
|
body: stringify(stripAllRenderFn(posts))
|
||||||
|
};
|
||||||
|
}
|
18
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/content-simple.astro
vendored
Normal file
18
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/content-simple.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from "astro:content";
|
||||||
|
const post = await getEntryBySlug('blog', 'simple');
|
||||||
|
const { Content } = await post.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Content - Simple</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Content />
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from "astro:content";
|
||||||
|
import Code from '../components/Code.astro';
|
||||||
|
import CustomMarquee from '../components/CustomMarquee.astro';
|
||||||
|
|
||||||
|
const post = await getEntryBySlug('blog', 'with-components');
|
||||||
|
const { Content } = await post.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Content - with components</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Content
|
||||||
|
components={{ CustomMarquee, Code }}
|
||||||
|
/>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
import { getEntryBySlug } from "astro:content";
|
||||||
|
|
||||||
|
const post = await getEntryBySlug('blog', 'with-config');
|
||||||
|
const { Content } = await post.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Content - with config</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Content />
|
||||||
|
</body>
|
||||||
|
</html>
|
10
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/entry.json.js
vendored
Normal file
10
packages/integrations/markdoc/test/fixtures/content-collections/src/pages/entry.json.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { getEntryBySlug } from 'astro:content';
|
||||||
|
import { stringify } from 'devalue';
|
||||||
|
import { stripRenderFn } from '../../utils.js';
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
const post = await getEntryBySlug('blog', 'simple');
|
||||||
|
return {
|
||||||
|
body: stringify(stripRenderFn(post)),
|
||||||
|
};
|
||||||
|
}
|
8
packages/integrations/markdoc/test/fixtures/content-collections/utils.js
vendored
Normal file
8
packages/integrations/markdoc/test/fixtures/content-collections/utils.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export function stripRenderFn(entryWithRender) {
|
||||||
|
const { render, ...entry } = entryWithRender;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripAllRenderFn(collection = []) {
|
||||||
|
return collection.map(stripRenderFn);
|
||||||
|
}
|
10
packages/integrations/markdoc/tsconfig.json
Normal file
10
packages/integrations/markdoc/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"module": "ES2020",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"target": "ES2020"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,14 +3,15 @@ import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/inter
|
||||||
import { compile as mdxCompile } from '@mdx-js/mdx';
|
import { compile as mdxCompile } from '@mdx-js/mdx';
|
||||||
import { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
import { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||||
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||||||
import type { AstroIntegration } from 'astro';
|
import type { AstroIntegration, ContentEntryType, HookParameters } from 'astro';
|
||||||
import { parse as parseESM } from 'es-module-lexer';
|
import { parse as parseESM } from 'es-module-lexer';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
|
||||||
import { VFile } from 'vfile';
|
import { VFile } from 'vfile';
|
||||||
import type { Plugin as VitePlugin } from 'vite';
|
import type { Plugin as VitePlugin } from 'vite';
|
||||||
import { getRehypePlugins, getRemarkPlugins, recmaInjectImportMetaEnvPlugin } from './plugins.js';
|
import { getRehypePlugins, getRemarkPlugins, recmaInjectImportMetaEnvPlugin } from './plugins.js';
|
||||||
import { getFileInfo, parseFrontmatter } from './utils.js';
|
import { getFileInfo, parseFrontmatter, ignoreStringPlugins } from './utils.js';
|
||||||
|
|
||||||
export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | 'rehypePlugins'> & {
|
export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | 'rehypePlugins'> & {
|
||||||
extendMarkdownConfig: boolean;
|
extendMarkdownConfig: boolean;
|
||||||
|
@ -22,19 +23,54 @@ export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | '
|
||||||
remarkRehype: RemarkRehypeOptions;
|
remarkRehype: RemarkRehypeOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroIntegration {
|
|
||||||
|
type IntegrationWithPrivateHooks = {
|
||||||
|
name: string;
|
||||||
|
hooks: Omit<AstroIntegration['hooks'], 'astro:config:setup'> & {
|
||||||
|
'astro:config:setup': (params: HookParameters<'astro:config:setup'> & {
|
||||||
|
// `addPageExtension` and `contentEntryType` are not a public APIs
|
||||||
|
// Add type defs here
|
||||||
|
addPageExtension: (extension: string) => void
|
||||||
|
addContentEntryType: (contentEntryType: ContentEntryType) => void
|
||||||
|
}) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): IntegrationWithPrivateHooks {
|
||||||
return {
|
return {
|
||||||
name: '@astrojs/mdx',
|
name: '@astrojs/mdx',
|
||||||
hooks: {
|
hooks: {
|
||||||
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
|
'astro:config:setup': async ({
|
||||||
|
updateConfig,
|
||||||
|
config,
|
||||||
|
addPageExtension,
|
||||||
|
addContentEntryType,
|
||||||
|
command,
|
||||||
|
}) => {
|
||||||
addPageExtension('.mdx');
|
addPageExtension('.mdx');
|
||||||
|
addContentEntryType({
|
||||||
|
extensions: ['.mdx'],
|
||||||
|
async getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) {
|
||||||
|
const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl));
|
||||||
|
return {
|
||||||
|
data: parsed.data,
|
||||||
|
body: parsed.content,
|
||||||
|
slug: parsed.data.slug,
|
||||||
|
rawData: parsed.matter,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contentModuleTypes: await fs.readFile(
|
||||||
|
new URL('../template/content-module-types.d.ts', import.meta.url),
|
||||||
|
'utf-8'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const extendMarkdownConfig =
|
const extendMarkdownConfig =
|
||||||
partialMdxOptions.extendMarkdownConfig ?? defaultOptions.extendMarkdownConfig;
|
partialMdxOptions.extendMarkdownConfig ?? defaultMdxOptions.extendMarkdownConfig;
|
||||||
|
|
||||||
const mdxOptions = applyDefaultOptions({
|
const mdxOptions = applyDefaultOptions({
|
||||||
options: partialMdxOptions,
|
options: partialMdxOptions,
|
||||||
defaults: extendMarkdownConfig ? config.markdown : defaultOptions,
|
defaults: markdownConfigToMdxOptions(extendMarkdownConfig ? config.markdown : markdownConfigDefaults),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mdxPluginOpts: MdxRollupPluginOptions = {
|
const mdxPluginOpts: MdxRollupPluginOptions = {
|
||||||
|
@ -149,14 +185,20 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: MdxOptions = {
|
const defaultMdxOptions = {
|
||||||
...markdownConfigDefaults,
|
|
||||||
extendMarkdownConfig: true,
|
extendMarkdownConfig: true,
|
||||||
recmaPlugins: [],
|
recmaPlugins: [],
|
||||||
remarkPlugins: [],
|
}
|
||||||
rehypePlugins: [],
|
|
||||||
remarkRehype: {},
|
function markdownConfigToMdxOptions(markdownConfig: typeof markdownConfigDefaults): MdxOptions {
|
||||||
|
return {
|
||||||
|
...defaultMdxOptions,
|
||||||
|
...markdownConfig,
|
||||||
|
remarkPlugins: ignoreStringPlugins(markdownConfig.remarkPlugins),
|
||||||
|
rehypePlugins: ignoreStringPlugins(markdownConfig.rehypePlugins),
|
||||||
|
remarkRehype: markdownConfig.remarkRehype as any ?? {},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function applyDefaultOptions({
|
function applyDefaultOptions({
|
||||||
options,
|
options,
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {
|
||||||
safelyGetAstroData,
|
safelyGetAstroData,
|
||||||
} from '@astrojs/markdown-remark/dist/internal.js';
|
} from '@astrojs/markdown-remark/dist/internal.js';
|
||||||
import { nodeTypes } from '@mdx-js/mdx';
|
import { nodeTypes } from '@mdx-js/mdx';
|
||||||
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
|
||||||
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||||||
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||||
import type { AstroConfig } from 'astro';
|
import type { AstroConfig } from 'astro';
|
||||||
import type { Literal, MemberExpression } from 'estree';
|
import type { Literal, MemberExpression } from 'estree';
|
||||||
import { visit as estreeVisit } from 'estree-util-visit';
|
import { visit as estreeVisit } from 'estree-util-visit';
|
||||||
import { bold, yellow } from 'kleur/colors';
|
import type { Image } from 'mdast';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkSmartypants from 'remark-smartypants';
|
import remarkSmartypants from 'remark-smartypants';
|
||||||
|
@ -21,6 +21,9 @@ import remarkPrism from './remark-prism.js';
|
||||||
import remarkShiki from './remark-shiki.js';
|
import remarkShiki from './remark-shiki.js';
|
||||||
import { jsToTreeNode } from './utils.js';
|
import { jsToTreeNode } from './utils.js';
|
||||||
|
|
||||||
|
// Skip nonessential plugins during performance benchmark runs
|
||||||
|
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
|
||||||
|
|
||||||
export function recmaInjectImportMetaEnvPlugin({
|
export function recmaInjectImportMetaEnvPlugin({
|
||||||
importMetaEnv,
|
importMetaEnv,
|
||||||
}: {
|
}: {
|
||||||
|
@ -99,15 +102,18 @@ export async function getRemarkPlugins(
|
||||||
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
||||||
let remarkPlugins: PluggableList = [];
|
let remarkPlugins: PluggableList = [];
|
||||||
|
|
||||||
|
if (!isPerformanceBenchmark) {
|
||||||
if (mdxOptions.gfm) {
|
if (mdxOptions.gfm) {
|
||||||
remarkPlugins.push(remarkGfm);
|
remarkPlugins.push(remarkGfm);
|
||||||
}
|
}
|
||||||
if (mdxOptions.smartypants) {
|
if (mdxOptions.smartypants) {
|
||||||
remarkPlugins.push(remarkSmartypants);
|
remarkPlugins.push(remarkSmartypants);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
remarkPlugins = [...remarkPlugins, ...ignoreStringPlugins(mdxOptions.remarkPlugins)];
|
remarkPlugins = [...remarkPlugins, ...mdxOptions.remarkPlugins];
|
||||||
|
|
||||||
|
if (!isPerformanceBenchmark) {
|
||||||
// Apply syntax highlighters after user plugins to match `markdown/remark` behavior
|
// Apply syntax highlighters after user plugins to match `markdown/remark` behavior
|
||||||
if (mdxOptions.syntaxHighlight === 'shiki') {
|
if (mdxOptions.syntaxHighlight === 'shiki') {
|
||||||
remarkPlugins.push([await remarkShiki(mdxOptions.shikiConfig)]);
|
remarkPlugins.push([await remarkShiki(mdxOptions.shikiConfig)]);
|
||||||
|
@ -115,6 +121,7 @@ export async function getRemarkPlugins(
|
||||||
if (mdxOptions.syntaxHighlight === 'prism') {
|
if (mdxOptions.syntaxHighlight === 'prism') {
|
||||||
remarkPlugins.push(remarkPrism);
|
remarkPlugins.push(remarkPrism);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return remarkPlugins;
|
return remarkPlugins;
|
||||||
}
|
}
|
||||||
|
@ -129,39 +136,16 @@ export function getRehypePlugins(mdxOptions: MdxOptions): MdxRollupPluginOptions
|
||||||
|
|
||||||
rehypePlugins = [
|
rehypePlugins = [
|
||||||
...rehypePlugins,
|
...rehypePlugins,
|
||||||
...ignoreStringPlugins(mdxOptions.rehypePlugins),
|
...mdxOptions.rehypePlugins,
|
||||||
// getHeadings() is guaranteed by TS, so this must be included.
|
// getHeadings() is guaranteed by TS, so this must be included.
|
||||||
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
|
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
|
||||||
rehypeHeadingIds,
|
...(isPerformanceBenchmark ? [] : [rehypeHeadingIds, rehypeInjectHeadingsExport]),
|
||||||
rehypeInjectHeadingsExport,
|
|
||||||
// computed from `astro.data.frontmatter` in VFile data
|
// computed from `astro.data.frontmatter` in VFile data
|
||||||
rehypeApplyFrontmatterExport,
|
rehypeApplyFrontmatterExport,
|
||||||
];
|
];
|
||||||
return rehypePlugins;
|
return rehypePlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ignoreStringPlugins(plugins: any[]) {
|
|
||||||
let validPlugins: PluggableList = [];
|
|
||||||
let hasInvalidPlugin = false;
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
if (typeof plugin === 'string') {
|
|
||||||
console.warn(yellow(`[MDX] ${bold(plugin)} not applied.`));
|
|
||||||
hasInvalidPlugin = true;
|
|
||||||
} else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
|
|
||||||
console.warn(yellow(`[MDX] ${bold(plugin[0])} not applied.`));
|
|
||||||
hasInvalidPlugin = true;
|
|
||||||
} else {
|
|
||||||
validPlugins.push(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasInvalidPlugin) {
|
|
||||||
console.warn(
|
|
||||||
`To inherit Markdown plugins in MDX, please use explicit imports in your config instead of "strings." See Markdown docs: https://docs.astro.build/en/guides/markdown-content/#markdown-plugins`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return validPlugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if estree entry is "import.meta.env.VARIABLE"
|
* Check if estree entry is "import.meta.env.VARIABLE"
|
||||||
* If it is, return the variable name (i.e. "VARIABLE")
|
* If it is, return the variable name (i.e. "VARIABLE")
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import type { Options as AcornOpts } from 'acorn';
|
import type { Options as AcornOpts } from 'acorn';
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
import type { AstroConfig, SSRError } from 'astro';
|
import type { AstroConfig, SSRError } from 'astro';
|
||||||
|
import { bold, yellow } from 'kleur/colors';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||||||
import type { MdxjsEsm } from 'mdast-util-mdx';
|
import type { MdxjsEsm } from 'mdast-util-mdx';
|
||||||
|
|
||||||
function appendForwardSlash(path: string) {
|
function appendForwardSlash(path: string) {
|
||||||
|
@ -82,3 +84,25 @@ export function jsToTreeNode(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ignoreStringPlugins(plugins: any[]): PluggableList {
|
||||||
|
let validPlugins: PluggableList = [];
|
||||||
|
let hasInvalidPlugin = false;
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
if (typeof plugin === 'string') {
|
||||||
|
console.warn(yellow(`[MDX] ${bold(plugin)} not applied.`));
|
||||||
|
hasInvalidPlugin = true;
|
||||||
|
} else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
|
||||||
|
console.warn(yellow(`[MDX] ${bold(plugin[0])} not applied.`));
|
||||||
|
hasInvalidPlugin = true;
|
||||||
|
} else {
|
||||||
|
validPlugins.push(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasInvalidPlugin) {
|
||||||
|
console.warn(
|
||||||
|
`To inherit Markdown plugins in MDX, please use explicit imports in your config instead of "strings." See Markdown docs: https://docs.astro.build/en/guides/markdown-content/#markdown-plugins`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return validPlugins;
|
||||||
|
}
|
||||||
|
|
9
packages/integrations/mdx/template/content-module-types.d.ts
vendored
Normal file
9
packages/integrations/mdx/template/content-module-types.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
declare module 'astro:content' {
|
||||||
|
interface Render {
|
||||||
|
'.mdx': Promise<{
|
||||||
|
Content: import('astro').MarkdownInstance<{}>['Content'];
|
||||||
|
headings: import('astro').MarkdownHeading[];
|
||||||
|
remarkPluginFrontmatter: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,9 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft
|
||||||
smartypants: true,
|
smartypants: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Skip nonessential plugins during performance benchmark runs
|
||||||
|
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
|
||||||
|
|
||||||
/** Shared utility for rendering markdown */
|
/** Shared utility for rendering markdown */
|
||||||
export async function renderMarkdown(
|
export async function renderMarkdown(
|
||||||
content: string,
|
content: string,
|
||||||
|
@ -64,13 +67,14 @@ export async function renderMarkdown(
|
||||||
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
||||||
.use([]);
|
.use([]);
|
||||||
|
|
||||||
|
if (!isPerformanceBenchmark && gfm) {
|
||||||
if (gfm) {
|
if (gfm) {
|
||||||
parser.use(remarkGfm);
|
parser.use(remarkGfm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smartypants) {
|
if (smartypants) {
|
||||||
parser.use(remarkSmartypants);
|
parser.use(remarkSmartypants);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
|
||||||
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
|
||||||
|
@ -79,6 +83,7 @@ export async function renderMarkdown(
|
||||||
parser.use([[plugin, pluginOpts]]);
|
parser.use([[plugin, pluginOpts]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isPerformanceBenchmark) {
|
||||||
if (scopedClassName) {
|
if (scopedClassName) {
|
||||||
parser.use([scopedStyles(scopedClassName)]);
|
parser.use([scopedStyles(scopedClassName)]);
|
||||||
}
|
}
|
||||||
|
@ -93,6 +98,7 @@ export async function renderMarkdown(
|
||||||
// Apply later in case user plugins resolve relative image paths
|
// Apply later in case user plugins resolve relative image paths
|
||||||
parser.use([toRemarkCollectImages(opts.resolveImage)]);
|
parser.use([toRemarkCollectImages(opts.resolveImage)]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parser.use([
|
parser.use([
|
||||||
[
|
[
|
||||||
|
@ -112,7 +118,11 @@ export async function renderMarkdown(
|
||||||
if (opts.experimentalAssets) {
|
if (opts.experimentalAssets) {
|
||||||
parser.use(rehypeImages(await opts.imageService, opts.assetsDir));
|
parser.use(rehypeImages(await opts.imageService, opts.assetsDir));
|
||||||
}
|
}
|
||||||
parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
|
if (!isPerformanceBenchmark) {
|
||||||
|
parser.use([rehypeHeadingIds]);
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.use([rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
|
||||||
|
|
||||||
let vfile: MarkdownVFile;
|
let vfile: MarkdownVFile;
|
||||||
try {
|
try {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue