fix markdown page script injection (#2871)
This commit is contained in:
parent
1061d6477a
commit
5029382a8c
14 changed files with 71 additions and 39 deletions
5
.changeset/cyan-pigs-ring.md
Normal file
5
.changeset/cyan-pigs-ring.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix a bug where tailwind integration wouldn't apply to markdown pages
|
|
@ -14,8 +14,9 @@ import Button from '../components/Button.astro';
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="grid place-items-center h-screen">
|
<div class="grid place-items-center h-screen content-center">
|
||||||
<Button>Click Me!</Button>
|
<Button>Tailwind Button in Astro!</Button>
|
||||||
|
<a href="/markdown-page" class="p-4 underline">Markdown is also supported...</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
10
examples/with-tailwindcss/src/pages/markdown-page.md
Normal file
10
examples/with-tailwindcss/src/pages/markdown-page.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
title: "Markdown + Tailwind"
|
||||||
|
setup: |
|
||||||
|
import Button from '../components/Button.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="grid place-items-center h-screen content-center">
|
||||||
|
<Button>Tailwind Button in Markdown!</Button>
|
||||||
|
<a href="/" class="p-4 underline">Go home...</a>
|
||||||
|
</div>
|
|
@ -465,7 +465,7 @@ export interface AstroUserConfig {
|
||||||
* - "page": Injected into the JavaScript bundle of every page. Processed & resolved by Vite.
|
* - "page": Injected into the JavaScript bundle of every page. Processed & resolved by Vite.
|
||||||
* - "page-ssr": Injected into the frontmatter of every Astro page. Processed & resolved by Vite.
|
* - "page-ssr": Injected into the frontmatter of every Astro page. Processed & resolved by Vite.
|
||||||
*/
|
*/
|
||||||
type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';
|
export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolved Astro Config
|
* Resolved Astro Config
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
|
||||||
import type { PageBuildData } from './types';
|
|
||||||
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
|
|
||||||
import type { StaticBuildOptions } from './types';
|
|
||||||
import type { BuildInternals } from '../../core/build/internal.js';
|
|
||||||
import type { RenderOptions } from '../../core/render/core';
|
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
|
||||||
import npath from 'path';
|
import npath from 'path';
|
||||||
|
import type { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import type { AstroConfig, AstroRenderer, ComponentInstance, EndpointHandler, SSRLoadedRenderer } from '../../@types/astro';
|
||||||
|
import type { BuildInternals } from '../../core/build/internal.js';
|
||||||
import { debug, error, info } from '../../core/logger.js';
|
import { debug, error, info } from '../../core/logger.js';
|
||||||
import { prependForwardSlash } from '../../core/path.js';
|
import { prependForwardSlash } from '../../core/path.js';
|
||||||
|
import type { RenderOptions } from '../../core/render/core';
|
||||||
import { resolveDependency } from '../../core/util.js';
|
import { resolveDependency } from '../../core/util.js';
|
||||||
|
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
import { call as callEndpoint } from '../endpoint/index.js';
|
import { call as callEndpoint } from '../endpoint/index.js';
|
||||||
import { render } from '../render/core.js';
|
import { render } from '../render/core.js';
|
||||||
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
import { createLinkStylesheetElementSet, createModuleScriptElementWithSrcSet } from '../render/ssr-element.js';
|
||||||
import { getOutRoot, getOutFolder, getOutFile } from './common.js';
|
import { getOutFile, getOutFolder, getOutRoot } from './common.js';
|
||||||
import { bgMagenta, black, cyan, dim, magenta } from 'kleur/colors';
|
import type { PageBuildData, StaticBuildOptions } from './types';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat } from './util.js';
|
||||||
|
|
||||||
|
|
||||||
// Render is usually compute, which Node.js can't parallelize well.
|
// Render is usually compute, which Node.js can't parallelize well.
|
||||||
// In real world testing, dropping from 10->1 showed a notiable perf
|
// In real world testing, dropping from 10->1 showed a notiable perf
|
||||||
// improvement. In the future, we can revisit a smarter parallel
|
// improvement. In the future, we can revisit a smarter parallel
|
||||||
|
@ -214,7 +214,7 @@ async function generatePath(pathname: string, opts: StaticBuildOptions, gopts: G
|
||||||
// Return this as placeholder, which will be ignored by the browser.
|
// Return this as placeholder, which will be ignored by the browser.
|
||||||
// TODO: In the future, we hope to run this entire script through Vite,
|
// TODO: In the future, we hope to run this entire script through Vite,
|
||||||
// removing the need to maintain our own custom Vite-mimic resolve logic.
|
// removing the need to maintain our own custom Vite-mimic resolve logic.
|
||||||
if (specifier === 'astro:scripts/before-hydration.js') {
|
if (specifier === BEFORE_HYDRATION_SCRIPT_ID) {
|
||||||
return 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
return 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||||
}
|
}
|
||||||
throw new Error(`Cannot find the built path for ${specifier}`);
|
throw new Error(`Cannot find the built path for ${specifier}`);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { SerializedRouteInfo, SerializedSSRManifest } from '../app/types';
|
||||||
|
|
||||||
import { chunkIsPage, rootRelativeFacadeId, getByFacadeId } from './generate.js';
|
import { chunkIsPage, rootRelativeFacadeId, getByFacadeId } from './generate.js';
|
||||||
import { serializeRouteData } from '../routing/index.js';
|
import { serializeRouteData } from '../routing/index.js';
|
||||||
|
import { BEFORE_HYDRATION_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
|
|
||||||
const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
const virtualModuleId = '@astrojs-ssr-virtual-entry';
|
||||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
@ -107,7 +108,7 @@ function buildManifest(bundle: OutputBundle, opts: StaticBuildOptions, internals
|
||||||
|
|
||||||
// HACK! Patch this special one.
|
// HACK! Patch this special one.
|
||||||
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
|
||||||
entryModules['astro:scripts/before-hydration.js'] = 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = 'data:text/javascript;charset=utf-8,//[no before-hydration script]';
|
||||||
|
|
||||||
const ssrManifest: SerializedSSRManifest = {
|
const ssrManifest: SerializedSSRManifest = {
|
||||||
routes,
|
routes,
|
||||||
|
|
|
@ -107,7 +107,8 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions,
|
||||||
: `await import("${await result.resolve(componentUrl)}");
|
: `await import("${await result.resolve(componentUrl)}");
|
||||||
return () => {};
|
return () => {};
|
||||||
`;
|
`;
|
||||||
|
// TODO: If we can figure out tree-shaking in the final SSR build, we could safely
|
||||||
|
// use BEFORE_HYDRATION_SCRIPT_ID instead of 'astro:scripts/before-hydration.js'.
|
||||||
const hydrationScript = {
|
const hydrationScript = {
|
||||||
props: { type: 'module', 'data-astro-component-hydration': true },
|
props: { type: 'module', 'data-astro-component-hydration': true },
|
||||||
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
|
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { cachedCompilation } from './compile.js';
|
||||||
import ancestor from 'common-ancestor-path';
|
import ancestor from 'common-ancestor-path';
|
||||||
import { trackCSSDependencies, handleHotUpdate } from './hmr.js';
|
import { trackCSSDependencies, handleHotUpdate } from './hmr.js';
|
||||||
import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
|
import { isRelativePath, startsWithForwardSlash } from '../core/path.js';
|
||||||
|
import { PAGE_SCRIPT_ID, PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
|
|
||||||
const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
|
const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
|
@ -93,9 +94,9 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
const filename = normalizeFilename(parsedId.filename);
|
const filename = normalizeFilename(parsedId.filename);
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
const fileUrl = new URL(`file://${filename}`);
|
||||||
let source = await fs.promises.readFile(fileUrl, 'utf-8');
|
let source = await fs.promises.readFile(fileUrl, 'utf-8');
|
||||||
const isPage = filename.startsWith(config.pages.pathname);
|
const isPage = fileUrl.pathname.startsWith(config.pages.pathname);
|
||||||
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
||||||
source += `\n<script hoist src="astro:scripts/page.js" />`;
|
source += `\n<script hoist src="${PAGE_SCRIPT_ID}" />`;
|
||||||
}
|
}
|
||||||
if (query.astro) {
|
if (query.astro) {
|
||||||
if (query.type === 'style') {
|
if (query.type === 'style') {
|
||||||
|
@ -152,7 +153,7 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
|
||||||
}
|
}
|
||||||
// Add handling to inject scripts into each page JS bundle, if needed.
|
// Add handling to inject scripts into each page JS bundle, if needed.
|
||||||
if (isPage) {
|
if (isPage) {
|
||||||
SUFFIX += `\nimport "astro:scripts/page-ssr.js";`;
|
SUFFIX += `\nimport "${PAGE_SSR_SCRIPT_ID}";`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: `${code}${SUFFIX}`,
|
code: `${code}${SUFFIX}`,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import esbuild from 'esbuild';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import type { AstroConfig } from '../@types/astro';
|
import type { AstroConfig } from '../@types/astro';
|
||||||
|
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
|
@ -27,7 +28,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
|
||||||
enforce: 'pre', // run transforms before other plugins can
|
enforce: 'pre', // run transforms before other plugins can
|
||||||
async load(id) {
|
async load(id) {
|
||||||
if (id.endsWith('.md')) {
|
if (id.endsWith('.md')) {
|
||||||
let source = await fs.promises.readFile(id, 'utf8');
|
const source = await fs.promises.readFile(id, 'utf8');
|
||||||
|
|
||||||
// Transform from `.md` to valid `.astro`
|
// Transform from `.md` to valid `.astro`
|
||||||
let render = config.markdownOptions.render;
|
let render = config.markdownOptions.render;
|
||||||
|
@ -42,13 +43,20 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
|
||||||
let renderResult = await render(source, renderOpts);
|
let renderResult = await render(source, renderOpts);
|
||||||
let { frontmatter, metadata, code: astroResult } = renderResult;
|
let { frontmatter, metadata, code: astroResult } = renderResult;
|
||||||
|
|
||||||
|
const filename = normalizeFilename(id);
|
||||||
|
const fileUrl = new URL(`file://${filename}`);
|
||||||
|
const isPage = fileUrl.pathname.startsWith(config.pages.pathname);
|
||||||
|
const hasInjectedScript = (isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr'));
|
||||||
|
|
||||||
// Extract special frontmatter keys
|
// Extract special frontmatter keys
|
||||||
const { layout = '', components = '', setup = '', ...content } = frontmatter;
|
const { layout = '', components = '', setup = '', ...content } = frontmatter;
|
||||||
content.astro = metadata;
|
content.astro = metadata;
|
||||||
const prelude = `---
|
const prelude = `---
|
||||||
${layout ? `import Layout from '${layout}';` : ''}
|
${layout ? `import Layout from '${layout}';` : ''}
|
||||||
${components ? `import * from '${components}';` : ''}
|
${components ? `import * from '${components}';` : ''}
|
||||||
|
${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''}
|
||||||
${setup}
|
${setup}
|
||||||
|
|
||||||
const $$content = ${JSON.stringify(content)}
|
const $$content = ${JSON.stringify(content)}
|
||||||
---`;
|
---`;
|
||||||
const imports = `${layout ? `import Layout from '${layout}';` : ''}
|
const imports = `${layout ? `import Layout from '${layout}';` : ''}
|
||||||
|
@ -60,12 +68,6 @@ ${setup}`.trim();
|
||||||
astroResult = `${prelude}\n${astroResult}`;
|
astroResult = `${prelude}\n${astroResult}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = normalizeFilename(id);
|
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
|
||||||
const isPage = filename.startsWith(config.pages.pathname);
|
|
||||||
if (isPage && config._ctx.scripts.some((s) => s.stage === 'page')) {
|
|
||||||
source += `\n<script hoist src="astro:scripts/page.js" />`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform from `.astro` to valid `.ts`
|
// Transform from `.astro` to valid `.ts`
|
||||||
let { code: tsResult } = await transform(astroResult, {
|
let { code: tsResult } = await transform(astroResult, {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Plugin as VitePlugin } from 'vite';
|
import { Plugin as VitePlugin } from 'vite';
|
||||||
import { AstroConfig } from '../@types/astro.js';
|
import { AstroConfig, InjectedScriptStage } from '../@types/astro.js';
|
||||||
|
|
||||||
// NOTE: We can't use the virtual "\0" ID convention because we need to
|
// NOTE: We can't use the virtual "\0" ID convention because we need to
|
||||||
// inject these as ESM imports into actual code, where they would not
|
// inject these as ESM imports into actual code, where they would not
|
||||||
// resolve correctly.
|
// resolve correctly.
|
||||||
const SCRIPT_ID_PREFIX = `astro:scripts/`;
|
const SCRIPT_ID_PREFIX = `astro:scripts/`;
|
||||||
const BEFORE_HYDRATION_SCRIPT_ID = `${SCRIPT_ID_PREFIX}before-hydration.js`;
|
export const BEFORE_HYDRATION_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'before-hydration' as InjectedScriptStage}.js`;
|
||||||
const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}page.js`;
|
export const PAGE_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page' as InjectedScriptStage}.js`;
|
||||||
const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}page-ssr.js`;
|
export const PAGE_SSR_SCRIPT_ID = `${SCRIPT_ID_PREFIX}${'page-ssr' as InjectedScriptStage}.js`;
|
||||||
|
|
||||||
export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin {
|
export default function astroScriptsPlugin({ config }: { config: AstroConfig }): VitePlugin {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,11 +2,6 @@
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import Button from '../components/Button.astro';
|
import Button from '../components/Button.astro';
|
||||||
import Complex from '../components/Complex.astro';
|
import Complex from '../components/Complex.astro';
|
||||||
|
|
||||||
import "../styles/global.css";
|
|
||||||
|
|
||||||
// Full Astro Component Syntax:
|
|
||||||
// https://docs.astro.build/core-concepts/astro-components/
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
11
packages/astro/test/fixtures/tailwindcss/src/pages/markdown-page.md
vendored
Normal file
11
packages/astro/test/fixtures/tailwindcss/src/pages/markdown-page.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
title: "Markdown + Tailwind"
|
||||||
|
setup: |
|
||||||
|
import Button from '../components/Button.astro';
|
||||||
|
import Complex from '../components/Complex.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="grid place-items-center h-screen content-center">
|
||||||
|
<Button>Tailwind Button in Markdown!</Button>
|
||||||
|
<Complex />
|
||||||
|
</div>
|
|
@ -1,3 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
|
@ -55,6 +55,14 @@ describe('Tailwind', () => {
|
||||||
expect(button.hasClass('w-10/12'), 'solidus').to.be.true;
|
expect(button.hasClass('w-10/12'), 'solidus').to.be.true;
|
||||||
expect(button.hasClass('2xl:w-[80%]'), 'complex class').to.be.true;
|
expect(button.hasClass('2xl:w-[80%]'), 'complex class').to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles Markdown pages', async () => {
|
||||||
|
const html = await fixture.readFile('/markdown-page/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const bundledCSSHREF = $('link[rel=stylesheet][href^=/assets/]').attr('href');
|
||||||
|
const bundledCSS = await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/'));
|
||||||
|
expect(bundledCSS, 'includes used component classes').to.match(/\.bg-purple-600{/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// with "build" handling CSS checking, the dev tests are mostly testing the paths resolve in dev
|
// with "build" handling CSS checking, the dev tests are mostly testing the paths resolve in dev
|
||||||
|
@ -73,8 +81,8 @@ describe('Tailwind', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves CSS in src/styles', async () => {
|
it('resolves CSS in src/styles', async () => {
|
||||||
const href = $(`link[href$="/src/styles/global.css"]`).attr('href');
|
const bundledCSSHREF = $('link[rel=stylesheet]').attr('href');
|
||||||
const res = await fixture.fetch(href);
|
const res = await fixture.fetch(bundledCSSHREF);
|
||||||
expect(res.status).to.equal(200);
|
expect(res.status).to.equal(200);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
Loading…
Reference in a new issue