Update tests
This commit is contained in:
parent
ddca8c4dea
commit
d8cd8a46ee
118 changed files with 4306 additions and 3217 deletions
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
// Component Imports
|
||||
import Counter from '../components/Counter.jsx'
|
||||
import Counter from '../components/Counter.tsx'
|
||||
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
@ -7,6 +8,7 @@
|
|||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Welcome to Astro</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.16.0",
|
||||
"@octokit/action": "^3.15.4",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.18.0",
|
||||
"autoprefixer": "^10.2.6",
|
||||
|
@ -56,11 +56,11 @@
|
|||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"execa": "^5.0.0",
|
||||
"jest": "^27.1.0",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"tiny-glob": "^0.2.8",
|
||||
"typescript": "^4.4.2",
|
||||
"uvu": "^0.5.1"
|
||||
"typescript": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
|
||||
import fetch from 'node-fetch';
|
||||
---
|
||||
import { renderMarkdown } from '@astrojs/markdown-support';
|
||||
import { __astro_slot } from 'astro/runtime/__astro_slot.js';
|
||||
|
||||
if(!('fetch' in globalThis)) {
|
||||
globalThis.fetch = fetch;
|
||||
export interface Props {
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// Internal props that should not be part of the external interface.
|
||||
interface InternalProps extends Props {
|
||||
$scope: string;
|
||||
}
|
||||
|
||||
const __TopLevelAstro = {
|
||||
site: new URL("http://localhost:3000"),
|
||||
|
@ -64,11 +66,11 @@ async function __render(props, ...children) {
|
|||
$scope
|
||||
} = Astro.props;
|
||||
let html = null;
|
||||
|
||||
// This flow is only triggered if a user passes `<Markdown content={content} />`
|
||||
if (content) {
|
||||
const {
|
||||
content: htmlContent
|
||||
} = await renderMarkdown(content, {
|
||||
mode: "md",
|
||||
const { content: htmlContent } = await renderMarkdown(content, {
|
||||
mode: 'md',
|
||||
$: {
|
||||
scopedClassName: $scope
|
||||
}
|
||||
|
@ -76,55 +78,9 @@ if (content) {
|
|||
html = htmlContent;
|
||||
}
|
||||
|
||||
return h(Fragment, null, h(Fragment, null,(html ? html : h(Fragment, null, h(__astro_slot, { [__astroContext]: props[__astroContext] }, children)))));
|
||||
}
|
||||
export default { isAstroComponent: true, __render };
|
||||
|
||||
// `__renderPage()`: Render the contents of the Astro module as a page. This is a special flow,
|
||||
// triggered by loading a component directly by URL.
|
||||
export async function __renderPage({request, children, props, css}) {
|
||||
const currentChild = {
|
||||
isAstroComponent: true,
|
||||
layout: typeof __layout === 'undefined' ? undefined : __layout,
|
||||
content: typeof __content === 'undefined' ? undefined : __content,
|
||||
__render,
|
||||
};
|
||||
|
||||
const isLayout = (__astroContext in props);
|
||||
if(!isLayout) {
|
||||
let astroRootUIDCounter = 0;
|
||||
Object.defineProperty(props, __astroContext, {
|
||||
value: {
|
||||
pageCSS: css,
|
||||
request,
|
||||
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(props, __astroInternal, {
|
||||
value: {
|
||||
isPage: !isLayout
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
const childBodyResult = await currentChild.__render(props, children);
|
||||
|
||||
// find layout, if one was given.
|
||||
if (currentChild.layout) {
|
||||
return currentChild.layout({
|
||||
request,
|
||||
props: {content: currentChild.content, [__astroContext]: props[__astroContext]},
|
||||
children: [childBodyResult],
|
||||
});
|
||||
}
|
||||
|
||||
return childBodyResult;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
If we have rendered `html` for `content`, render that
|
||||
Otherwise, just render the slotted content
|
||||
*/
|
||||
---
|
||||
{html ? html : <slot />}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"./debug": "./components/Debug.astro",
|
||||
"./components/*": "./components/*",
|
||||
"./package.json": "./package.json",
|
||||
"./runtime/*": "./dist/runtime/*"
|
||||
"./runtime/*": "./dist/runtime/*.js",
|
||||
"./internal": "./dist/internal/index.js"
|
||||
},
|
||||
"imports": {
|
||||
"#astro/*": "./dist/*.js"
|
||||
|
@ -35,12 +36,12 @@
|
|||
"dev": "astro-scripts dev \"src/**/*.ts\"",
|
||||
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
||||
"test": "uvu test -i fixtures -i benchmark -i test-utils.js"
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^0.1.0-canary.36",
|
||||
"@astrojs/language-server": "^0.7.16",
|
||||
"@astrojs/markdown-support": "0.3.1",
|
||||
"@astrojs/parser": "0.20.2",
|
||||
"@astrojs/prism": "0.2.2",
|
||||
"@astrojs/renderer-preact": "0.2.2",
|
||||
"@astrojs/renderer-react": "0.2.2",
|
||||
|
@ -68,10 +69,12 @@
|
|||
"es-module-lexer": "^0.7.1",
|
||||
"esbuild": "^0.12.23",
|
||||
"estree-util-value-to-estree": "^1.2.0",
|
||||
"etag": "^1.8.1",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"fdir": "^5.1.0",
|
||||
"kleur": "^4.1.4",
|
||||
"mime": "^2.5.2",
|
||||
"morphdom": "^2.6.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"picomatch": "^2.3.0",
|
||||
|
@ -85,7 +88,7 @@
|
|||
"srcset-parse": "^1.1.0",
|
||||
"string-width": "^5.0.0",
|
||||
"supports-esm": "^1.0.0",
|
||||
"vite": "^2.5.1",
|
||||
"vite": "^2.5.2",
|
||||
"yargs-parser": "^20.2.9",
|
||||
"zod": "^3.8.1"
|
||||
},
|
||||
|
@ -93,6 +96,7 @@
|
|||
"@astrojs/parser": "^0.20.2",
|
||||
"@types/babel__core": "^7.1.15",
|
||||
"@types/connect": "^3.4.35",
|
||||
"@types/etag": "^1.8.1",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/send": "^0.17.1",
|
||||
|
|
|
@ -3,15 +3,16 @@ import type babel from '@babel/core';
|
|||
import type vite from 'vite';
|
||||
import type { z } from 'zod';
|
||||
import type { AstroConfigSchema } from '../config';
|
||||
import type { AstroComponentFactory } from '../internal';
|
||||
|
||||
export { AstroMarkdownOptions };
|
||||
|
||||
export interface AstroComponentMetadata {
|
||||
displayName: string;
|
||||
hydrate?: 'load' | 'idle' | 'visible' | 'media' | 'only';
|
||||
hydrateArgs?: any;
|
||||
componentUrl?: string;
|
||||
componentExport?: { value: string; namespace?: boolean };
|
||||
value?: undefined | string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -140,12 +141,7 @@ export interface CollectionRSS {
|
|||
|
||||
/** Generic interface for a component (Astro, Svelte, React, etc.) */
|
||||
export interface ComponentInstance {
|
||||
default: {
|
||||
isAstroComponent: boolean;
|
||||
__render?(props: Props, ...children: any[]): string;
|
||||
__renderer?: Renderer;
|
||||
};
|
||||
__renderPage?: (options: RenderPageOptions) => string;
|
||||
default: AstroComponentFactory;
|
||||
css?: string[];
|
||||
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
|
||||
}
|
||||
|
|
45
packages/astro/src/@types/compiler.d.ts
vendored
Normal file
45
packages/astro/src/@types/compiler.d.ts
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
declare module '@astrojs/compiler' {
|
||||
export type Platform = 'browser' | 'node' | 'neutral';
|
||||
export type SourceMap = any;
|
||||
|
||||
export interface TransformOptions {}
|
||||
|
||||
export interface TransformResult {
|
||||
code: string;
|
||||
map: SourceMap;
|
||||
warnings: any[];
|
||||
}
|
||||
|
||||
export interface TransformResults {
|
||||
js: TransformResult;
|
||||
css?: TransformResult;
|
||||
}
|
||||
|
||||
// This function transforms a single JavaScript file. It can be used to minify
|
||||
// JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript
|
||||
// to older JavaScript. It returns a promise that is either resolved with a
|
||||
// "TransformResult" object or rejected with a "TransformFailure" object.
|
||||
//
|
||||
// Works in node: yes
|
||||
// Works in browser: yes
|
||||
export declare function transform(input: string, options?: TransformOptions): Promise<string>;
|
||||
|
||||
// This configures the browser-based version of astro. It is necessary to
|
||||
// call this first and wait for the returned promise to be resolved before
|
||||
// making other API calls when using astro in the browser.
|
||||
//
|
||||
// Works in node: yes
|
||||
// Works in browser: yes ("options" is required)
|
||||
export declare function initialize(options: InitializeOptions): Promise<void>;
|
||||
|
||||
export interface InitializeOptions {
|
||||
// The URL of the "astro.wasm" file. This must be provided when running
|
||||
// astro in the browser.
|
||||
wasmURL?: string;
|
||||
|
||||
// By default astro runs the WebAssembly-based browser API in a web worker
|
||||
// to avoid blocking the UI thread. This can be disabled by setting "worker"
|
||||
// to false.
|
||||
worker?: boolean;
|
||||
}
|
||||
}
|
|
@ -75,7 +75,7 @@ class AstroBuilder {
|
|||
// static pages
|
||||
if (pathname) {
|
||||
allPages.push(
|
||||
ssr({ filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname, viteServer }).then((html) => ({
|
||||
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname, viteServer }).then((html) => ({
|
||||
html,
|
||||
name: pathname.replace(/\/?$/, '/index.html').replace(/^\//, ''),
|
||||
}))
|
||||
|
@ -97,7 +97,7 @@ class AstroBuilder {
|
|||
// TODO: throw error if conflict
|
||||
staticPaths.paths.forEach((staticPath) => {
|
||||
allPages.push(
|
||||
ssr({ filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname: staticPath, viteServer }).then((html) => ({
|
||||
ssr({ astroConfig: this.config, filePath, logging, mode: 'production', origin, route, routeCache: this.routeCache, pathname: staticPath, viteServer }).then((html) => ({
|
||||
html,
|
||||
name: staticPath.replace(/\/?$/, '/index.html').replace(/^\//, ''),
|
||||
}))
|
||||
|
|
|
@ -10,7 +10,7 @@ import { z } from 'zod';
|
|||
import { defaultLogDestination } from '../logger.js';
|
||||
import build from '../build/index.js';
|
||||
import devServer from '../dev/index.js';
|
||||
import { preview } from '../preview/index.js';
|
||||
import preview from '../preview/index.js';
|
||||
import { reload } from './reload.js';
|
||||
import { check } from './check.js';
|
||||
import { formatConfigError, loadConfig } from '../config.js';
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import type { NextFunction } from 'connect';
|
||||
import type http from 'http';
|
||||
import type { AstroConfig, ManifestData, RouteCache } from '../@types/astro';
|
||||
import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
import type { HmrContext, ModuleNode } from 'vite';
|
||||
|
||||
import chokidar from 'chokidar';
|
||||
import connect from 'connect';
|
||||
import mime from 'mime';
|
||||
import getEtag from 'etag';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import vite from 'vite';
|
||||
import { defaultLogOptions, error, info } from '../logger.js';
|
||||
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
|
||||
|
@ -16,6 +21,8 @@ import { loadViteConfig } from '../runtime/vite/config.js';
|
|||
import * as msg from './messages.js';
|
||||
import { errorTemplate } from './template/error.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export interface DevOptions {
|
||||
logging: LogOptions;
|
||||
}
|
||||
|
@ -39,16 +46,18 @@ export default async function dev(config: AstroConfig, options: DevOptions = { l
|
|||
hostname: server.hostname,
|
||||
port: server.port,
|
||||
server: server.app,
|
||||
stop: server.stop,
|
||||
stop: () => server.stop(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Dev server */
|
||||
class AstroDevServer {
|
||||
export class AstroDevServer {
|
||||
app = connect();
|
||||
httpServer: http.Server | undefined;
|
||||
hostname: string;
|
||||
port: number;
|
||||
|
||||
private internalCache: Map<string, string>;
|
||||
private config: AstroConfig;
|
||||
private logging: LogOptions;
|
||||
private manifest: ManifestData;
|
||||
|
@ -56,8 +65,10 @@ class AstroDevServer {
|
|||
private routeCache: RouteCache = {};
|
||||
private viteServer: vite.ViteDevServer | undefined;
|
||||
private watcher: chokidar.FSWatcher;
|
||||
private mostRecentRoute?: RouteData;
|
||||
|
||||
constructor(config: AstroConfig, options: DevOptions) {
|
||||
this.internalCache = new Map();
|
||||
this.config = config;
|
||||
this.hostname = config.devOptions.hostname || 'localhost';
|
||||
this.logging = options.logging;
|
||||
|
@ -94,7 +105,7 @@ class AstroDevServer {
|
|||
host: this.hostname,
|
||||
},
|
||||
},
|
||||
{ astroConfig: this.config, logging: this.logging }
|
||||
{ astroConfig: this.config, logging: this.logging, devServer: this }
|
||||
);
|
||||
this.viteServer = await vite.createServer(viteConfig);
|
||||
|
||||
|
@ -104,27 +115,24 @@ class AstroDevServer {
|
|||
this.app.use((req, res, next) => this.renderError(req, res, next));
|
||||
|
||||
// 4. listen on port
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.app
|
||||
.listen(this.port, this.hostname, () => {
|
||||
info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
|
||||
info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}` }));
|
||||
resolve();
|
||||
})
|
||||
.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code && err.code === 'EADDRINUSE') {
|
||||
error(this.logging, 'astro', `Address ${this.hostname}:${this.port} already in use. Try changing devOptions.port in your config file`);
|
||||
} else {
|
||||
error(this.logging, 'astro', err.stack);
|
||||
}
|
||||
reject();
|
||||
process.exit(1);
|
||||
});
|
||||
this.httpServer = this.app.listen(this.port, this.hostname, () => {
|
||||
info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart }));
|
||||
info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}` }));
|
||||
});
|
||||
this.httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code && err.code === 'EADDRINUSE') {
|
||||
error(this.logging, 'astro', `Address ${this.hostname}:${this.port} already in use. Try changing devOptions.port in your config file`);
|
||||
} else {
|
||||
error(this.logging, 'astro', err.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
/** Stop dev server */
|
||||
async stop() {
|
||||
this.internalCache = new Map();
|
||||
if (this.httpServer) this.httpServer.close(); // close HTTP server
|
||||
await Promise.all([
|
||||
...(this.viteServer ? [this.viteServer.close()] : []), // close Vite server
|
||||
this.watcher.close(), // close chokidar
|
||||
|
@ -138,6 +146,13 @@ class AstroDevServer {
|
|||
let pathname = req.url || '/'; // original request
|
||||
const reqStart = performance.now();
|
||||
|
||||
if (pathname.startsWith('/@astro')) {
|
||||
const spec = pathname.slice(2);
|
||||
const url = await this.viteServer.moduleGraph.resolveUrl(spec);
|
||||
req.url = url[1];
|
||||
return this.viteServer.middlewares.handle(req, res, next);
|
||||
}
|
||||
|
||||
try {
|
||||
const route = matchRoute(pathname, this.manifest);
|
||||
|
||||
|
@ -147,8 +162,11 @@ class AstroDevServer {
|
|||
return;
|
||||
}
|
||||
|
||||
this.mostRecentRoute = route;
|
||||
|
||||
// handle .astro and .md pages
|
||||
const html = await ssr({
|
||||
astroConfig: this.config,
|
||||
filePath: new URL(`./${route.component}`, this.config.projectRoot),
|
||||
logging: this.logging,
|
||||
mode: 'development',
|
||||
|
@ -168,7 +186,7 @@ class AstroDevServer {
|
|||
} catch (e) {
|
||||
const err = e as Error;
|
||||
this.viteServer.ssrFixStacktrace(err);
|
||||
console.error(err.stack);
|
||||
console.log(err.stack);
|
||||
const statusCode = 500;
|
||||
const html = errorTemplate({ statusCode, title: 'Internal Error', tabTitle: '500: Error', message: err.message });
|
||||
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
|
||||
|
@ -195,6 +213,7 @@ class AstroDevServer {
|
|||
const userDefined404 = this.manifest.routes.find((route) => route.component === relPages + '404.astro');
|
||||
if (userDefined404) {
|
||||
html = await ssr({
|
||||
astroConfig: this.config,
|
||||
filePath: new URL(`./${userDefined404.component}`, this.config.projectRoot),
|
||||
logging: this.logging,
|
||||
mode: 'development',
|
||||
|
@ -216,4 +235,53 @@ class AstroDevServer {
|
|||
res.write(html);
|
||||
res.end();
|
||||
}
|
||||
|
||||
public async handleHotUpdate({ file, modules }: HmrContext): Promise<void | ModuleNode[]> {
|
||||
if (!this.viteServer) throw new Error(`AstroDevServer.start() not called`);
|
||||
|
||||
for (const module of modules) {
|
||||
this.viteServer.moduleGraph.invalidateModule(module);
|
||||
}
|
||||
|
||||
const route = this.mostRecentRoute;
|
||||
const pathname = route?.pathname ?? '/';
|
||||
|
||||
if (!route) {
|
||||
this.viteServer.ws.send({
|
||||
type: 'full-reload',
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// try to update the most recent route
|
||||
const html = await ssr({
|
||||
astroConfig: this.config,
|
||||
filePath: new URL(`./${route.component}`, this.config.projectRoot),
|
||||
logging: this.logging,
|
||||
mode: 'development',
|
||||
origin: this.origin,
|
||||
pathname,
|
||||
route,
|
||||
routeCache: this.routeCache,
|
||||
viteServer: this.viteServer,
|
||||
});
|
||||
|
||||
// TODO: log update
|
||||
this.viteServer.ws.send({
|
||||
type: 'custom',
|
||||
event: 'astro:reload',
|
||||
data: { html },
|
||||
});
|
||||
return [];
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
this.viteServer.ssrFixStacktrace(err);
|
||||
console.log(err.stack);
|
||||
this.viteServer.ws.send({
|
||||
type: 'full-reload',
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,12 @@ export function req({ url, statusCode, reqTime }: { url: string; statusCode: num
|
|||
return `${color(statusCode)} ${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
|
||||
}
|
||||
|
||||
/** Display */
|
||||
export function reload({ url, reqTime }: { url: string; reqTime: number }): string {
|
||||
let color = yellow;
|
||||
return `${pad(url, 40)} ${dim(Math.round(reqTime) + 'ms')}`;
|
||||
}
|
||||
|
||||
/** Display dev server host and startup time */
|
||||
export function devStart({ startupTime }: { startupTime: number }): string {
|
||||
return `${pad(`Server started`, 44)} ${dim(`${Math.round(startupTime)}ms`)}`;
|
||||
|
|
211
packages/astro/src/internal/index.ts
Normal file
211
packages/astro/src/internal/index.ts
Normal file
|
@ -0,0 +1,211 @@
|
|||
import type { AstroComponentMetadata } from '../@types/astro';
|
||||
import { renderAstroComponent } from '../runtime/ssr.js';
|
||||
|
||||
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
||||
import * as astring from 'astring';
|
||||
import shorthash from 'shorthash';
|
||||
const { generate, GENERATOR } = astring;
|
||||
// A more robust version alternative to `JSON.stringify` that can handle most values
|
||||
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
||||
const customGenerator: astring.Generator = {
|
||||
...GENERATOR,
|
||||
Literal(node, state) {
|
||||
if (node.raw != null) {
|
||||
// escape closing script tags in strings so browsers wouldn't interpret them as
|
||||
// closing the actual end tag in HTML
|
||||
state.write(node.raw.replace('</script>', '<\\/script>'));
|
||||
} else {
|
||||
GENERATOR.Literal(node, state);
|
||||
}
|
||||
},
|
||||
};
|
||||
const serialize = (value: Value) =>
|
||||
generate(valueToEstree(value), {
|
||||
generator: customGenerator,
|
||||
});
|
||||
|
||||
async function _render(child: any) {
|
||||
// Special: If a child is a function, call it automatically.
|
||||
// This lets you do {() => ...} without the extra boilerplate
|
||||
// of wrapping it in a function and calling it.
|
||||
if (typeof child === 'function') {
|
||||
return await child();
|
||||
} else if (typeof child === 'string') {
|
||||
return child;
|
||||
} else if (!child && child !== 0) {
|
||||
// do nothing, safe to ignore falsey values.
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
export class AstroComponent {
|
||||
private htmlParts: string[];
|
||||
private expressions: TemplateStringsArray;
|
||||
|
||||
constructor(htmlParts: string[], expressions: TemplateStringsArray) {
|
||||
this.htmlParts = htmlParts;
|
||||
this.expressions = expressions;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
const { htmlParts, expressions } = this;
|
||||
|
||||
for (let i = 0; i < htmlParts.length; i++) {
|
||||
const html = htmlParts[i];
|
||||
const expression = expressions[i];
|
||||
|
||||
yield _render(html);
|
||||
yield _render(expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function render(htmlParts: string[], ...expressions: TemplateStringsArray) {
|
||||
return new AstroComponent(htmlParts, expressions);
|
||||
}
|
||||
|
||||
export interface AstroComponentFactory {
|
||||
(result: any, props: any, slots: any): ReturnType<typeof render>;
|
||||
isAstroComponentFactory?: boolean;
|
||||
}
|
||||
|
||||
export const createComponent = (cb: AstroComponentFactory) => {
|
||||
// Add a flag to this callback to mark it as an Astro component
|
||||
(cb as any).isAstroComponentFactory = true;
|
||||
return cb;
|
||||
}
|
||||
|
||||
function extractHydrationDirectives(inputProps: Record<string | number, any>): { hydrationDirective: [string, any] | null; props: Record<string | number, any> } {
|
||||
let props: Record<string | number, any> = {};
|
||||
let hydrationDirective: [string, any] | null = null;
|
||||
for (const [key, value] of Object.entries(inputProps)) {
|
||||
if (key.startsWith('client:')) {
|
||||
hydrationDirective = [key.split(':')[1], value];
|
||||
} else {
|
||||
props[key] = value;
|
||||
}
|
||||
}
|
||||
return { hydrationDirective, props };
|
||||
}
|
||||
|
||||
interface HydrateScriptOptions {
|
||||
renderer: any;
|
||||
astroId: string;
|
||||
props: any;
|
||||
}
|
||||
|
||||
/** For hydrated components, generate a <script type="module"> to load the component */
|
||||
async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>) {
|
||||
const { renderer, astroId, props } = scriptOptions;
|
||||
const { hydrate, componentUrl, componentExport } = metadata;
|
||||
|
||||
if (!componentExport) {
|
||||
throw new Error(`Unable to resolve a componentExport for "${metadata.displayName}"! Please open an issue.`)
|
||||
}
|
||||
|
||||
let hydrationSource = '';
|
||||
if (renderer.hydrationPolyfills) {
|
||||
hydrationSource += `await Promise.all([${renderer.hydrationPolyfills.map((src: string) => `\n import("${src}")`).join(', ')}]);\n`;
|
||||
}
|
||||
|
||||
hydrationSource += renderer.source
|
||||
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]);
|
||||
return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
|
||||
`
|
||||
: `await import("${componentUrl}");
|
||||
return () => {};
|
||||
`;
|
||||
|
||||
const hydrationScript = `<script type="module">
|
||||
import setup from 'astro/client/${hydrate}.js';
|
||||
setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => {
|
||||
${hydrationSource}
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
|
||||
return hydrationScript;
|
||||
}
|
||||
|
||||
export const renderComponent = async (result: any, displayName: string, Component: unknown, _props: Record<string | number, any>, children: any) => {
|
||||
Component = await Component;
|
||||
// children = await renderGenerator(children);
|
||||
const { renderers } = result._metadata;
|
||||
if (Component && (Component as any).isAstroComponentFactory) {
|
||||
const output = await renderAstroComponent(await (Component as any)(result, Component, _props, children))
|
||||
return output;
|
||||
}
|
||||
|
||||
let metadata: AstroComponentMetadata = { displayName };
|
||||
|
||||
if (Component == null) {
|
||||
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
|
||||
}
|
||||
// else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
|
||||
// throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
|
||||
// }
|
||||
const { hydrationDirective, props } = extractHydrationDirectives(_props);
|
||||
let html = '';
|
||||
|
||||
if (!hydrationDirective) {
|
||||
return '<pre>Not implemented</pre>';
|
||||
}
|
||||
metadata.hydrate = hydrationDirective[0] as AstroComponentMetadata['hydrate'];
|
||||
metadata.hydrateArgs = hydrationDirective[1];
|
||||
|
||||
for (const [url, exported] of Object.entries(result._metadata.importedModules)) {
|
||||
for (const [key, value] of Object.entries(exported as any)) {
|
||||
if (Component === value) {
|
||||
metadata.componentExport = { value: key };
|
||||
metadata.componentUrl = url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = null;
|
||||
for (const r of renderers) {
|
||||
if (await r.ssr.check(Component, props, null)) {
|
||||
renderer = r;
|
||||
}
|
||||
}
|
||||
|
||||
({ html } = await renderer.ssr.renderToStaticMarkup(Component, props, null));
|
||||
const astroId = shorthash.unique(html);
|
||||
|
||||
result.scripts.add(await generateHydrateScript({ renderer, astroId, props }, metadata as Required<AstroComponentMetadata>));
|
||||
|
||||
return `<astro-root uid="${astroId}">${html}</astro-root>`;
|
||||
};
|
||||
|
||||
export const addAttribute = (value: any, key: string) => {
|
||||
if (value == null || value === false) {
|
||||
return '';
|
||||
}
|
||||
return ` ${key}="${value}"`;
|
||||
};
|
||||
|
||||
export const spreadAttributes = (values: Record<any, any>) => {
|
||||
let output = '';
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
output += addAttribute(value, key);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
export const defineStyleVars = (astroId: string, vars: Record<any, any>) => {
|
||||
let output = '\n';
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
output += ` --${key}: ${value};\n`;
|
||||
}
|
||||
return `.${astroId} {${output}}`;
|
||||
};
|
||||
|
||||
export const defineScriptVars = (vars: Record<any, any>) => {
|
||||
let output = '';
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
output += `let ${key} = ${JSON.stringify(value)};\n`;
|
||||
}
|
||||
return output;
|
||||
};
|
|
@ -13,7 +13,7 @@ interface PreviewOptions {
|
|||
}
|
||||
|
||||
/** The primary dev action */
|
||||
export async function preview(config: AstroConfig, { logging }: PreviewOptions) {
|
||||
export default async function preview(config: AstroConfig, { logging }: PreviewOptions) {
|
||||
const {
|
||||
dist,
|
||||
devOptions: { hostname, port },
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
import type { AstroComponentMetadata, ComponentInstance, Renderer } from '../@types/astro';
|
||||
|
||||
import hash from 'shorthash';
|
||||
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
||||
import * as astring from 'astring';
|
||||
import * as astroHtml from './html.js';
|
||||
|
||||
const { generate, GENERATOR } = astring;
|
||||
|
||||
// A more robust version alternative to `JSON.stringify` that can handle most values
|
||||
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
||||
const customGenerator: astring.Generator = {
|
||||
...GENERATOR,
|
||||
Literal(node, state) {
|
||||
if (node.raw != null) {
|
||||
// escape closing script tags in strings so browsers wouldn't interpret them as
|
||||
// closing the actual end tag in HTML
|
||||
state.write(node.raw.replace('</script>', '<\\/script>'));
|
||||
} else {
|
||||
GENERATOR.Literal(node, state);
|
||||
}
|
||||
},
|
||||
};
|
||||
const serialize = (value: Value) =>
|
||||
generate(valueToEstree(value), {
|
||||
generator: customGenerator,
|
||||
});
|
||||
|
||||
declare let rendererInstances: Renderer[];
|
||||
|
||||
function isCustomElementTag(name: unknown) {
|
||||
return typeof name === 'string' && /-/.test(name);
|
||||
}
|
||||
|
||||
const rendererCache = new Map<any, Renderer>();
|
||||
|
||||
/** For client:only components, attempt to infer the required renderer. */
|
||||
function inferClientRenderer(metadata: Partial<AstroComponentMetadata>) {
|
||||
// If there's only one renderer, assume it's the required renderer
|
||||
if (rendererInstances.length === 1) {
|
||||
return rendererInstances[0];
|
||||
} else if (metadata.value) {
|
||||
// Attempt to find the renderer by matching the hydration value
|
||||
const hint = metadata.value;
|
||||
let match = rendererInstances.find((instance) => instance.name === hint);
|
||||
|
||||
if (!match) {
|
||||
// Didn't find an exact match, try shorthand hints for the internal renderers
|
||||
const fullHintName = `@astrojs/renderer-${hint}`;
|
||||
match = rendererInstances.find((instance) => instance.name === fullHintName);
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Couldn't find a renderer for <${metadata.displayName} client:only="${metadata.value}" />. Is there a renderer that matches the "${metadata.value}" hint in your Astro config?`
|
||||
);
|
||||
}
|
||||
return match;
|
||||
} else {
|
||||
// Multiple renderers included but no hint was provided
|
||||
throw new Error(
|
||||
`Can't determine the renderer for ${metadata.displayName}. Include a hint similar to <${metadata.displayName} client:only="react" /> when multiple renderers are included in your Astro config.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** For a given component, resolve the renderer. Results are cached if this instance is encountered again */
|
||||
async function resolveRenderer(Component: any, props: any = {}, children?: string, metadata: Partial<AstroComponentMetadata> = {}): Promise<Renderer | undefined> {
|
||||
// For client:only components, the component can't be imported
|
||||
// during SSR. We need to infer the required renderer.
|
||||
if (metadata.hydrate === 'only') {
|
||||
return inferClientRenderer(metadata);
|
||||
}
|
||||
|
||||
if (rendererCache.has(Component)) {
|
||||
return rendererCache.get(Component);
|
||||
}
|
||||
|
||||
const errors: Error[] = [];
|
||||
for (const renderer of rendererInstances) {
|
||||
// Yes, we do want to `await` inside of this loop!
|
||||
// __renderer.check can't be run in parallel, it
|
||||
// returns the first match and skips any subsequent checks
|
||||
try {
|
||||
const shouldUse: boolean = await renderer.ssr.check(Component, props, children);
|
||||
|
||||
if (shouldUse) {
|
||||
rendererCache.set(Component, renderer);
|
||||
return renderer;
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push(err as any);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
// For now just throw the first error we encounter.
|
||||
throw errors[0];
|
||||
}
|
||||
}
|
||||
|
||||
interface HydrateScriptOptions {
|
||||
renderer: Renderer;
|
||||
astroId: string;
|
||||
props: any;
|
||||
}
|
||||
|
||||
const astroHtmlRendererInstance: Renderer = {
|
||||
name: '@astrojs/renderer-html',
|
||||
source: '',
|
||||
ssr: astroHtml as any,
|
||||
polyfills: [],
|
||||
hydrationPolyfills: [],
|
||||
};
|
||||
|
||||
/** For hydrated components, generate a <script type="module"> to load the component */
|
||||
async function generateHydrateScript(scriptOptions: HydrateScriptOptions, metadata: Required<AstroComponentMetadata>) {
|
||||
const { renderer, astroId, props } = scriptOptions;
|
||||
const { hydrate, componentUrl, componentExport } = metadata;
|
||||
|
||||
let hydrationSource = '';
|
||||
if (renderer.hydrationPolyfills) {
|
||||
hydrationSource += `await Promise.all([${renderer.hydrationPolyfills.map((src) => `\n import("${src}")`).join(', ')}]);\n`;
|
||||
}
|
||||
|
||||
hydrationSource += renderer.source
|
||||
? `const [{ ${componentExport.value}: Component }, { default: hydrate }] = await Promise.all([import("${componentUrl}"), import("${renderer.source}")]);
|
||||
return (el, children) => hydrate(el)(Component, ${serialize(props)}, children);
|
||||
`
|
||||
: `await import("${componentUrl}");
|
||||
return () => {};
|
||||
`;
|
||||
|
||||
const hydrationScript = `<script type="module">
|
||||
import setup from 'astro/client/${hydrate}.js';
|
||||
setup("${astroId}", {${metadata.value ? `value: "${metadata.value}"` : ''}}, async () => {
|
||||
${hydrationSource}
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
|
||||
return hydrationScript;
|
||||
}
|
||||
|
||||
const getComponentName = (Component: any, componentProps: any) => {
|
||||
if (componentProps.displayName) return componentProps.displayName;
|
||||
switch (typeof Component) {
|
||||
case 'function':
|
||||
return Component.displayName ?? Component.name;
|
||||
case 'string':
|
||||
return Component;
|
||||
default: {
|
||||
return Component;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const prepareSlottedChildren = (children: string | Record<any, any>[]) => {
|
||||
const $slots: Record<string, string> = {
|
||||
default: '',
|
||||
};
|
||||
for (const child of children) {
|
||||
if (typeof child === 'string') {
|
||||
$slots.default += child;
|
||||
} else if (typeof child === 'object' && child['$slot']) {
|
||||
if (!$slots[child['$slot']]) $slots[child['$slot']] = '';
|
||||
$slots[child['$slot']] += child.children.join('').replace(new RegExp(`slot="${child['$slot']}"\s*`, ''));
|
||||
}
|
||||
}
|
||||
|
||||
return { $slots };
|
||||
};
|
||||
|
||||
const removeSlottedChildren = (_children: string | Record<any, any>[]) => {
|
||||
let children = '';
|
||||
for (const child of _children) {
|
||||
if (typeof child === 'string') {
|
||||
children += child;
|
||||
} else if (typeof child === 'object' && child['$slot']) {
|
||||
children += child.children.join('');
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
/** The main wrapper for any components in Astro files */
|
||||
export function __astro_component(Component: ComponentInstance['default'], metadata: AstroComponentMetadata = {} as any) {
|
||||
if (Component == null) {
|
||||
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
|
||||
} else if (typeof Component === 'string' && !isCustomElementTag(Component)) {
|
||||
throw new Error(`Astro is unable to render ${metadata.displayName}!\nIs there a renderer to handle this type of component defined in your Astro config?`);
|
||||
}
|
||||
|
||||
return async function __astro_component_internal(props: any, ..._children: any[]) {
|
||||
if (Component.isAstroComponent && Component.__render) {
|
||||
return Component.__render(props, prepareSlottedChildren(_children));
|
||||
}
|
||||
const children = removeSlottedChildren(_children);
|
||||
let renderer = await resolveRenderer(Component, props, children, metadata);
|
||||
|
||||
if (!renderer) {
|
||||
if (isCustomElementTag(Component as any)) {
|
||||
renderer = astroHtmlRendererInstance;
|
||||
} else {
|
||||
// If the user only specifies a single renderer, but the check failed
|
||||
// for some reason... just default to their preferred renderer.
|
||||
renderer = rendererInstances.length === 2 ? rendererInstances[1] : undefined;
|
||||
}
|
||||
|
||||
if (!renderer) {
|
||||
const name = getComponentName(Component, metadata);
|
||||
throw new Error(`No renderer found for ${name}! Did you forget to add a renderer to your Astro config?`);
|
||||
}
|
||||
}
|
||||
|
||||
let html = '';
|
||||
// Skip SSR for components using client:only hydration
|
||||
if (metadata.hydrate !== 'only') {
|
||||
const rendered = await renderer.ssr.renderToStaticMarkup(Component, props, children, metadata);
|
||||
html = rendered.html;
|
||||
}
|
||||
|
||||
if (renderer.polyfills) {
|
||||
let polyfillScripts = renderer.polyfills.map((src: string) => `<script type="module" src="${src}"></script>`).join('');
|
||||
html = html + polyfillScripts;
|
||||
}
|
||||
|
||||
// If we're NOT hydrating this component, just return the HTML
|
||||
if (!metadata.hydrate) {
|
||||
// It's safe to remove <astro-fragment>, static content doesn't need the wrapper
|
||||
return html.replace(/\<\/?astro-fragment\>/g, '');
|
||||
}
|
||||
|
||||
// If we ARE hydrating this component, let's generate the hydration script
|
||||
const uniqueId = props[Symbol.for('astro.context')].createAstroRootUID(html);
|
||||
const uniqueIdHashed = hash.unique(uniqueId);
|
||||
const script = await generateHydrateScript({ renderer, astroId: uniqueIdHashed, props }, metadata as Required<AstroComponentMetadata>);
|
||||
const astroRoot = `<astro-root uid="${uniqueIdHashed}">${html}</astro-root>`;
|
||||
return [astroRoot, script].join('\n');
|
||||
};
|
||||
}
|
33
packages/astro/src/runtime/hmr.ts
Normal file
33
packages/astro/src/runtime/hmr.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'vite/client';
|
||||
|
||||
if (import.meta.hot) {
|
||||
const parser = new DOMParser();
|
||||
import.meta.hot.on('astro:reload', async ({ html }: { html: string }) => {
|
||||
const { default: morphdom } = await import('morphdom');
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
morphdom(document.head, doc.head, {
|
||||
onBeforeElUpdated: function (fromEl, toEl) {
|
||||
if (fromEl.isEqualNode(toEl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
morphdom(document.body, doc.body, {
|
||||
onBeforeElUpdated: function (fromEl, toEl) {
|
||||
if (fromEl.localName === 'astro-root') {
|
||||
return fromEl.getAttribute('uid') !== toEl.getAttribute('uid');
|
||||
}
|
||||
|
||||
if (fromEl.isEqualNode(toEl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
import cheerio from 'cheerio';
|
||||
import * as eslexer from 'es-module-lexer';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
import type { ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode } from '../@types/astro';
|
||||
import type { ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, AstroConfig } from '../@types/astro';
|
||||
import type { LogOptions } from '../logger';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { generatePaginateFunction } from './paginate.js';
|
||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||
import { canonicalURL, parseNpmName } from './util.js';
|
||||
import { parseNpmName, canonicalURL as getCanonicalURL } from './util.js';
|
||||
import type { AstroComponent, AstroComponentFactory } from '../internal';
|
||||
|
||||
interface SSROptions {
|
||||
/** an instance of the AstroConfig */
|
||||
astroConfig: AstroConfig,
|
||||
/** location of file on disk */
|
||||
filePath: URL;
|
||||
/** logging options */
|
||||
|
@ -32,11 +36,129 @@ interface SSROptions {
|
|||
// this prevents client-side errors such as the "double React bug" (https://reactjs.org/warnings/invalid-hook-call-warning.html#mismatching-versions-of-react-and-react-dom)
|
||||
let browserHash: string | undefined;
|
||||
|
||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
||||
let template = '';
|
||||
|
||||
for await (const value of component) {
|
||||
if (value || value === 0) {
|
||||
template += value;
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
export async function renderToString(result: any, componentFactory: AstroComponentFactory, props: any, children: any) {
|
||||
const Component = componentFactory(result, props, children);
|
||||
let template = await renderAstroComponent(Component);
|
||||
return template
|
||||
}
|
||||
|
||||
async function renderPage(result: any, Component: AstroComponentFactory, props: any, children: any) {
|
||||
const template = await renderToString(result, Component, props, children);
|
||||
const styles = Array.from(result.styles).map(style => `<style>${style}</style>`);
|
||||
const scripts = Array.from(result.scripts);
|
||||
return template.replace("</head>", styles.join('\n') + scripts.join('\n') + "</head>");
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
// TODO: improve validation and error handling here.
|
||||
async function resolveRenderers(viteServer: ViteDevServer, ids: string[]) {
|
||||
const resolve = viteServer.config.createResolver();
|
||||
const renderers = await Promise.all(ids.map(async renderer => {
|
||||
if (cache.has(renderer)) return cache.get(renderer);
|
||||
const resolvedRenderer: any = {};
|
||||
|
||||
// We can dynamically import the renderer by itself because it shouldn't have
|
||||
// any non-standard imports, the index is just meta info.
|
||||
// The other entrypoints need to be loaded through Vite.
|
||||
const { default: instance } = await import(renderer);
|
||||
|
||||
// This resolves the renderer's entrypoints to a final URL through Vite
|
||||
const getPath = async (src: string) => {
|
||||
const spec = path.posix.join(instance.name, src);
|
||||
const resolved = await resolve(spec);
|
||||
if (!resolved) {
|
||||
throw new Error(`Unable to resolve "${spec}" to a package!`)
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
resolvedRenderer.name = instance.name;
|
||||
if (instance.client) {
|
||||
resolvedRenderer.source = await getPath(instance.client);
|
||||
}
|
||||
if (Array.isArray(instance.hydrationPolyfills)) {
|
||||
resolvedRenderer.hydrationPolyfills = await Promise.all(instance.hydrationPolyfills.map((src: string) => getPath(src)));
|
||||
}
|
||||
if (Array.isArray(instance.polyfills)) {
|
||||
resolvedRenderer.polyfills = await Promise.all(instance.polyfills.map((src: string) => getPath(src)));
|
||||
}
|
||||
|
||||
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(await getPath(instance.server));
|
||||
const { default: server } = await viteServer.ssrLoadModule(url);
|
||||
resolvedRenderer.ssr = server;
|
||||
|
||||
cache.set(renderer, resolvedRenderer);
|
||||
return resolvedRenderer
|
||||
}));
|
||||
|
||||
return renderers;
|
||||
}
|
||||
|
||||
async function resolveImportedModules(viteServer: ViteDevServer, file: string) {
|
||||
const { url } = await viteServer.moduleGraph.ensureEntryFromUrl(file);
|
||||
const modulesByFile = viteServer.moduleGraph.getModulesByFile(url);
|
||||
if (!modulesByFile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let importedModules: Record<string, any> = {};
|
||||
const moduleNodes = Array.from(modulesByFile);
|
||||
|
||||
// Loop over the importedModules and grab the exports from each one.
|
||||
// We'll pass these to the shared $$result so renderers can match
|
||||
// components to their exported identifier and URL
|
||||
// NOTE: Important that this is parallelized as much as possible!
|
||||
await Promise.all(moduleNodes.map(moduleNode => {
|
||||
const entries = Array.from(moduleNode.importedModules);
|
||||
|
||||
return Promise.all(entries.map(entry => {
|
||||
// Skip our internal import that every module will have
|
||||
if (entry.id?.endsWith('astro/dist/internal/index.js')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return viteServer.moduleGraph.ensureEntryFromUrl(entry.url).then(mod => {
|
||||
if (mod.ssrModule) {
|
||||
importedModules[mod.url] = mod.ssrModule;
|
||||
return;
|
||||
} else {
|
||||
return viteServer.ssrLoadModule(mod.url).then(result => {
|
||||
importedModules[mod.url] = result.ssrModule;
|
||||
return;
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
}))
|
||||
|
||||
return importedModules
|
||||
}
|
||||
|
||||
/** use Vite to SSR */
|
||||
export async function ssr({ filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
|
||||
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
|
||||
// 1. load module
|
||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||
|
||||
// 1.5. resolve renderers and imported modules.
|
||||
// important that this happens _after_ ssrLoadModule, otherwise `importedModules` would be empty
|
||||
const [renderers, importedModules] = await Promise.all([
|
||||
resolveRenderers(viteServer, astroConfig.renderers),
|
||||
resolveImportedModules(viteServer, fileURLToPath(filePath))
|
||||
]);
|
||||
|
||||
// 2. handle dynamic routes
|
||||
let params: Params = {};
|
||||
let pageProps: Props = {};
|
||||
|
@ -68,24 +190,34 @@ export async function ssr({ filePath, logging, mode, origin, pathname, route, ro
|
|||
// 3. render page
|
||||
if (!browserHash && (viteServer as any)._optimizeDepsMetadata?.browserHash) browserHash = (viteServer as any)._optimizeDepsMetadata.browserHash; // note: this is "private" and may change over time
|
||||
const fullURL = new URL(pathname, origin);
|
||||
if (!mod.__renderPage) throw new Error(`__renderPage() undefined (${route?.component})`);
|
||||
let html = await mod.__renderPage({
|
||||
request: {
|
||||
params,
|
||||
url: fullURL,
|
||||
canonicalURL: canonicalURL(fullURL.pathname, fullURL.origin),
|
||||
|
||||
const Component = await mod.default;
|
||||
if (!Component)
|
||||
throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
|
||||
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||
|
||||
let html = await renderPage({
|
||||
styles: new Set(),
|
||||
scripts: new Set(),
|
||||
/** This function returns the `Astro` faux-global */
|
||||
createAstro(props: any) {
|
||||
const site = new URL(origin);
|
||||
const url = new URL('.' + pathname, site);
|
||||
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin)
|
||||
return { isPage: true, site, request: { url, canonicalURL }, props };
|
||||
},
|
||||
children: [],
|
||||
props: pageProps,
|
||||
css: mod.css || [],
|
||||
});
|
||||
_metadata: { importedModules, renderers },
|
||||
}, Component, { }, null);
|
||||
|
||||
// 4. modify response
|
||||
// inject Vite HMR code (dev only)
|
||||
if (mode === 'development') html = injectViteClient(html);
|
||||
|
||||
// replace client hydration scripts
|
||||
if (mode === 'development') html = resolveNpmImports(html);
|
||||
if (mode === 'development') {
|
||||
// inject Astro HMR code
|
||||
html = injectAstroHMR(html);
|
||||
// inject Vite HMR code
|
||||
html = injectViteClient(html);
|
||||
// replace client hydration scripts
|
||||
html = resolveNpmImports(html);
|
||||
}
|
||||
|
||||
// 5. finish
|
||||
return html;
|
||||
|
@ -96,6 +228,11 @@ function injectViteClient(html: string): string {
|
|||
return html.replace('<head>', `<head><script type="module" src="/@vite/client"></script>`);
|
||||
}
|
||||
|
||||
/** Injects Astro HMR client code */
|
||||
function injectAstroHMR(html: string): string {
|
||||
return html.replace('<head>', `<head><script type="module" src="/@astro/runtime/hmr"></script>`);
|
||||
}
|
||||
|
||||
/** Convert npm specifier into Vite URL */
|
||||
function resolveViteNpmPackage(spec: string): string {
|
||||
const pkg = parseNpmName(spec);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { createRequire } from 'module';
|
|||
import { getPackageJSON, parseNpmName } from '../util.js';
|
||||
import astro from './plugin-astro.js';
|
||||
import jsx from './plugin-jsx.js';
|
||||
import { AstroDevServer } from '../../dev';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
|
@ -17,7 +18,7 @@ const require = createRequire(import.meta.url);
|
|||
type ViteConfigWithSSR = InlineConfig & { ssr?: { external?: string[]; noExternal?: string[] } };
|
||||
|
||||
/** Return a common starting point for all Vite actions */
|
||||
export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfig, logging }: { astroConfig: AstroConfig; logging: LogOptions }): Promise<ViteConfigWithSSR> {
|
||||
export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfig, logging, devServer }: { astroConfig: AstroConfig; logging: LogOptions, devServer?: AstroDevServer }): Promise<ViteConfigWithSSR> {
|
||||
const optimizedDeps = new Set<string>(); // dependencies that must be bundled for the client (Vite may not detect all of these)
|
||||
const dedupe = new Set<string>(); // dependencies that can’t be duplicated (e.g. React & SolidJS)
|
||||
const plugins: Plugin[] = []; // Vite plugins
|
||||
|
@ -73,7 +74,7 @@ export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfi
|
|||
/** Always include these dependencies for optimization */
|
||||
include: [...optimizedDeps],
|
||||
},
|
||||
plugins: [astro(astroConfig), jsx({ config: astroConfig, logging }), ...plugins],
|
||||
plugins: [astro({ config: astroConfig, devServer }), jsx({ config: astroConfig, logging }), ...plugins],
|
||||
publicDir: fileURLToPath(astroConfig.public),
|
||||
resolve: {
|
||||
dedupe: [...dedupe],
|
||||
|
@ -82,6 +83,8 @@ export async function loadViteConfig(viteConfig: ViteConfigWithSSR, { astroConfi
|
|||
server: {
|
||||
/** prevent serving outside of project root (will become new default soon) */
|
||||
fs: { strict: true },
|
||||
/** disable HMR for test */
|
||||
hmr: process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production' ? false : undefined,
|
||||
/** handle Vite URLs */
|
||||
proxy: {
|
||||
// add proxies here
|
||||
|
|
|
@ -1,58 +1,36 @@
|
|||
import type { Plugin } from 'vite';
|
||||
import type { AstroConfig, Renderer } from '../../@types/astro.js';
|
||||
import type { LogOptions } from '../../logger';
|
||||
|
||||
import { camelCase } from 'camel-case';
|
||||
import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { markdownToH } from '../markdown.js';
|
||||
import { transform } from '@astrojs/compiler';
|
||||
import { AstroDevServer } from '../../dev/index.js';
|
||||
|
||||
interface AstroPluginOptions {
|
||||
config: AstroConfig;
|
||||
devServer?: AstroDevServer;
|
||||
}
|
||||
|
||||
/** Transform .astro files for Vite */
|
||||
export default function astro(config: AstroConfig): Plugin {
|
||||
export default function astro({ devServer }: AstroPluginOptions): Plugin {
|
||||
return {
|
||||
name: '@astrojs/vite-plugin-astro',
|
||||
enforce: 'pre', // run transforms before other plugins can
|
||||
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
|
||||
async load(id) {
|
||||
if (id.endsWith('.astro') || id.endsWith('.md')) {
|
||||
// TODO: replace with compiler
|
||||
let code = await fs.promises.readFile(id, 'utf8');
|
||||
return {
|
||||
code: code,
|
||||
map: null,
|
||||
};
|
||||
}
|
||||
// inject renderers (TODO: improve this?)
|
||||
if (id.endsWith('runtime/__astro_component.js')) {
|
||||
let code = await fs.promises.readFile(id, 'utf8');
|
||||
let rendererCode = '';
|
||||
let source = await fs.promises.readFile(id, 'utf8');
|
||||
|
||||
// add imports
|
||||
config.renderers.forEach((name) => {
|
||||
rendererCode += `import ${jsRef(name)} from '${name}';
|
||||
import ${jsRef(name, '_ssr')} from '${name}/server';
|
||||
`;
|
||||
});
|
||||
// 1. Transform from `.astro` to valid `.ts`
|
||||
const tsResult = await transform(source, { sourcefile: id });
|
||||
|
||||
// initialize renderers
|
||||
rendererCode += `
|
||||
function initRenderer(name, entry, ssr) {
|
||||
const join = (...parts) => parts.map((part) => part.replace(/^\\./, '')).join('');
|
||||
const renderer = {};
|
||||
renderer.name = name;
|
||||
renderer.ssr = ssr;
|
||||
if (entry.client) renderer.source = join(name, entry.client);
|
||||
if (Array.isArray(entry.hydrationPolyfills)) renderer.hydrationPolyfills = entry.hydrationPolyfills.map((src) => join(name, src));
|
||||
if (Array.isArray(entry.polyfills)) renderer.polyfills = entry.polyfills.map((src) => join(name, src));
|
||||
return renderer;
|
||||
}
|
||||
let rendererInstances = [
|
||||
${config.renderers.map((name) => `initRenderer('${name}', ${jsRef(name)}, ${jsRef(name, '_ssr')})`).join(',\n')}
|
||||
];
|
||||
`;
|
||||
// 2. Compile `.ts` to `.js`
|
||||
const { code, map } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
|
||||
|
||||
return {
|
||||
code: rendererCode + code,
|
||||
map: null,
|
||||
code,
|
||||
map,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -64,34 +42,10 @@ let rendererInstances = [
|
|||
// }
|
||||
return null;
|
||||
},
|
||||
async handleHotUpdate({ file, modules, timestamp, server, read }) {
|
||||
// invalidate module
|
||||
const module = server.moduleGraph.getModuleById(file);
|
||||
if (module) server.moduleGraph.invalidateModule(module);
|
||||
|
||||
try {
|
||||
const {
|
||||
default: { __render: render },
|
||||
} = await server.ssrLoadModule(file);
|
||||
const html = await render();
|
||||
server.ws.send({
|
||||
type: 'custom',
|
||||
event: 'astro:reload',
|
||||
data: {
|
||||
html,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
server.ws.send({
|
||||
type: 'full-reload',
|
||||
});
|
||||
async handleHotUpdate(context) {
|
||||
if (devServer) {
|
||||
return devServer.handleHotUpdate(context);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Given any string (e.g. npm package name), generate a JS-friendly ref */
|
||||
function jsRef(name: string, suffix = ''): string {
|
||||
return `__${camelCase(name)}${suffix}`;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import build from '../dist/build/index.js';
|
||||
import { loadFixture } from './test-utils';
|
||||
|
||||
const Assets = suite('Assets');
|
||||
let fixture;
|
||||
|
||||
await build({ projectRoot: 'test/fixtures/astro-assets/' });
|
||||
describe('Assets', () => {
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-assets/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
Assets('build the base image', () => {});
|
||||
test('built the base image', async () => {
|
||||
await fixture.readFile('/images/twitter.png');
|
||||
});
|
||||
|
||||
// let oneX = await readFile('/_astro/src/images/twitter.png');
|
||||
// assert.ok(oneX, 'built the base image');
|
||||
test('built the 2x image', async () => {
|
||||
await fixture.readFile('/images/twitter@2x.png');
|
||||
});
|
||||
|
||||
// let twoX = await readFile('/_astro/src/images/twitter@2x.png');
|
||||
// assert.ok(twoX, 'built the 2x image');
|
||||
|
||||
// let threeX = await readFile('/_astro/src/images/twitter@3x.png');
|
||||
// assert.ok(threeX, 'build the 3x image');
|
||||
|
||||
Assets.run();
|
||||
test('built the 3x image', async () => {
|
||||
await fixture.readFile('/images/twitter@3x.png');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,59 +1,62 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Attributes = suite('Attributes test');
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Attributes, './fixtures/astro-attrs');
|
||||
describe('Attributes', () => {
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-attrs/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Attributes('Passes attributes to elements as expected', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Passes attributes to elements as expected', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.html());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
const attrs = {
|
||||
'false-str': 'false',
|
||||
'true-str': 'true',
|
||||
false: undefined,
|
||||
true: '',
|
||||
empty: '',
|
||||
null: undefined,
|
||||
undefined: undefined,
|
||||
};
|
||||
|
||||
const ids = ['false-str', 'true-str', 'false', 'true', 'empty', 'null', 'undefined'];
|
||||
const specs = ['false', 'true', undefined, '', '', undefined, undefined];
|
||||
for (const [k, v] of Object.entries(attrs)) {
|
||||
const attr = $(`#${k}`).attr('attr');
|
||||
expect(attr).toBe(v);
|
||||
}
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
for (const id of ids) {
|
||||
const spec = specs[i];
|
||||
const attr = $(`#${id}`).attr('attr');
|
||||
assert.equal(attr, spec, `Passes ${id} as "${spec}"`);
|
||||
i++;
|
||||
}
|
||||
test('Passes boolean attributes to components as expected', async () => {
|
||||
const html = await fixture.fetch('/component').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#true').attr('attr')).toBe('attr-true');
|
||||
expect($('#true').attr('type')).toBe('boolean');
|
||||
expect($('#false').attr('attr')).toBe('attr-false');
|
||||
expect($('#false').attr('type')).toBe('boolean');
|
||||
});
|
||||
|
||||
test('Passes namespaced attributes as expected', async () => {
|
||||
const html = await fixture.fetch('/namespaced').then((res) => res.text());
|
||||
const $ = cheerio.load(result.contents);
|
||||
|
||||
expect($('div').attr('xmlns:happy')).toBe('https://example.com/schemas/happy');
|
||||
expect($('img').attr('happy:smile')).toBe('sweet');
|
||||
});
|
||||
|
||||
test('Passes namespaced attributes to components as expected', async () => {
|
||||
const html = await fixture.fetch('/namespaced-component');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('span').attr('on:click')).toEqual(Function.prototype.toString.call((event) => console.log(event)));
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
Attributes('Passes boolean attributes to components as expected', async ({ runtime }) => {
|
||||
const result = await runtime.load('/component');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#true').attr('attr'), 'attr-true');
|
||||
assert.equal($('#true').attr('type'), 'boolean');
|
||||
assert.equal($('#false').attr('attr'), 'attr-false');
|
||||
assert.equal($('#false').attr('type'), 'boolean');
|
||||
});
|
||||
|
||||
Attributes('Passes namespaced attributes as expected', async ({ runtime }) => {
|
||||
const result = await runtime.load('/namespaced');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('div').attr('xmlns:happy'), 'https://example.com/schemas/happy');
|
||||
assert.equal($('img').attr('happy:smile'), 'sweet');
|
||||
});
|
||||
|
||||
Attributes('Passes namespaced attributes to components as expected', async ({ runtime }) => {
|
||||
const result = await runtime.load('/namespaced-component');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal(
|
||||
$('span').attr('on:click'),
|
||||
Function.prototype.toString.call((event) => console.log(event))
|
||||
);
|
||||
});
|
||||
|
||||
Attributes.run();
|
||||
|
|
|
@ -1,123 +1,109 @@
|
|||
import { suite } from 'uvu';
|
||||
import http from 'http';
|
||||
import { promisify } from 'util';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild, setupPreview } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Basics = suite('Basic test');
|
||||
describe('Astro basics', () => {
|
||||
describe('dev', () => {
|
||||
let fixture; // fixture #1. Note that .dev() and .preview() share a port, so these fixtures must be kept separate.
|
||||
let devServer;
|
||||
|
||||
setup(Basics, './fixtures/astro-basic', {
|
||||
runtimeOptions: {
|
||||
mode: 'development',
|
||||
},
|
||||
});
|
||||
setupBuild(Basics, './fixtures/astro-basic');
|
||||
setupPreview(Basics, './fixtures/astro-basic');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-basic/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Basics('Can load page', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Can load page', async () => {
|
||||
const html = await fixture.fetch(`/`).then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
expect($('h1').text()).toBe('Hello world!');
|
||||
});
|
||||
|
||||
assert.equal($('h1').text(), 'Hello world!');
|
||||
});
|
||||
test('Correctly serializes boolean attributes', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
Basics('Sets the HMR port when dynamic components used', async ({ runtime }) => {
|
||||
const result = await runtime.load('/client');
|
||||
const html = result.contents;
|
||||
assert.ok(/HMR_WEBSOCKET_PORT/.test(html), 'Sets the websocket port');
|
||||
});
|
||||
expect($('h1').attr('data-something')).toBe('');
|
||||
expect($('h2').attr('not-data-ok')).toBe('');
|
||||
});
|
||||
|
||||
Basics('Correctly serializes boolean attributes', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('h1').attr('data-something'), '');
|
||||
assert.equal($('h2').attr('not-data-ok'), '');
|
||||
});
|
||||
test('Selector with an empty body', async () => {
|
||||
const html = await fixture.fetch('/empty-class').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
Basics('Selector with an empty body', async ({ runtime }) => {
|
||||
const result = await runtime.load('/empty-class');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('.author').length, 1, 'author class added');
|
||||
});
|
||||
expect($('.author')).toHaveLength(1);
|
||||
});
|
||||
|
||||
Basics('Build does not include HMR client', async ({ build, readFile }) => {
|
||||
await build().catch((err) => {
|
||||
assert.ok(!err, 'Error during the build');
|
||||
test('Allows forward-slashes in mustache tags (#407)', async () => {
|
||||
const html = await fixture.fetch('/forward-slash').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('a[href="/post/one"]')).toHaveLength(1);
|
||||
expect($('a[href="/post/two"]')).toHaveLength(1);
|
||||
expect($('a[href="/post/three"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Allows spread attributes (#521)', async () => {
|
||||
const html = await fixture.fetch('/spread').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#spread-leading')).toHaveLength(1);
|
||||
expect($('#spread-leading').attr('a')).toBe('0');
|
||||
expect($('#spread-leading').attr('b')).toBe('1');
|
||||
expect($('#spread-leading').attr('c')).toBe('2');
|
||||
|
||||
expect($('#spread-trailing')).toHaveLength(1);
|
||||
expect($('#spread-trailing').attr('a')).toBe('0');
|
||||
expect($('#spread-trailing').attr('b')).toBe('1');
|
||||
expect($('#spread-trailing').attr('c')).toBe('2');
|
||||
});
|
||||
|
||||
test('Allows spread attributes with TypeScript (#521)', async () => {
|
||||
const html = await fixture.fetch('/spread').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#spread-ts')).toHaveLength(1);
|
||||
expect($('#spread-ts').attr('a')).toBe('0');
|
||||
expect($('#spread-ts').attr('b')).toBe('1');
|
||||
expect($('#spread-ts').attr('c')).toBe('2');
|
||||
});
|
||||
|
||||
test('Allows using the Fragment element to be used', async () => {
|
||||
const html = await fixture.fetch('/fragment').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// will be 1 if element rendered correctly
|
||||
expect($('#one')).toHaveLength(1);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
const clientHTML = await readFile('/client/index.html');
|
||||
const $ = doc(clientHTML);
|
||||
|
||||
assert.equal($('script[src="/_snowpack/hmr-client.js"]').length, 0, 'No HMR client script');
|
||||
const hmrPortScript = $('script').filter((i, el) => {
|
||||
return $(el)
|
||||
.text()
|
||||
.match(/window\.HMR_WEBSOCKET_PORT/);
|
||||
describe('preview', () => {
|
||||
let fixture; // fixture #2. Note that .dev() and .preview() share a port, so these fixtures must be kept separate.
|
||||
let previewServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-basic' });
|
||||
await fixture.build();
|
||||
previewServer = await fixture.preview();
|
||||
});
|
||||
|
||||
test('returns 200 for valid URLs', async () => {
|
||||
const result = fixture.fetch('/');
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns 404 for invalid URLs', async () => {
|
||||
const result = fixture.fetch('/bad-url');
|
||||
expect(result.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
// important: close preview server (free up port and connection)
|
||||
afterAll(() => {
|
||||
previewServer.close();
|
||||
});
|
||||
});
|
||||
assert.equal(hmrPortScript.length, 0, 'No script setting the websocket port');
|
||||
});
|
||||
|
||||
Basics('Preview server works as expected', async ({ build, previewServer }) => {
|
||||
await build().catch((err) => {
|
||||
assert.ok(!err, 'Error during the build');
|
||||
});
|
||||
{
|
||||
const resultOrError = await promisify(http.get)(`http://localhost:${previewServer.address().port}/`).catch((err) => err);
|
||||
assert.equal(resultOrError.statusCode, 200);
|
||||
}
|
||||
{
|
||||
const resultOrError = await promisify(http.get)(`http://localhost:${previewServer.address().port}/bad-url`).catch((err) => err);
|
||||
assert.equal(resultOrError.statusCode, 404);
|
||||
}
|
||||
});
|
||||
|
||||
Basics('Allows forward-slashes in mustache tags (#407)', async ({ runtime }) => {
|
||||
const result = await runtime.load('/forward-slash');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('a[href="/post/one"]').length, 1);
|
||||
assert.equal($('a[href="/post/two"]').length, 1);
|
||||
assert.equal($('a[href="/post/three"]').length, 1);
|
||||
});
|
||||
|
||||
Basics('Allows spread attributes (#521)', async ({ runtime }) => {
|
||||
const result = await runtime.load('/spread');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('#spread-leading').length, 1);
|
||||
assert.equal($('#spread-leading').attr('a'), '0');
|
||||
assert.equal($('#spread-leading').attr('b'), '1');
|
||||
assert.equal($('#spread-leading').attr('c'), '2');
|
||||
|
||||
assert.equal($('#spread-trailing').length, 1);
|
||||
assert.equal($('#spread-trailing').attr('a'), '0');
|
||||
assert.equal($('#spread-trailing').attr('b'), '1');
|
||||
assert.equal($('#spread-trailing').attr('c'), '2');
|
||||
});
|
||||
|
||||
Basics('Allows spread attributes with TypeScript (#521)', async ({ runtime }) => {
|
||||
const result = await runtime.load('/spread');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('#spread-ts').length, 1);
|
||||
assert.equal($('#spread-ts').attr('a'), '0');
|
||||
assert.equal($('#spread-ts').attr('b'), '1');
|
||||
assert.equal($('#spread-ts').attr('c'), '2');
|
||||
});
|
||||
|
||||
Basics('Allows using the Fragment element to be used', async ({ runtime }) => {
|
||||
const result = await runtime.load('/fragment');
|
||||
assert.ok(!result.error, 'No errors thrown');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('#one').length, 1, 'Element in a fragment rendered');
|
||||
});
|
||||
|
||||
Basics.run();
|
||||
|
|
|
@ -1,75 +1,81 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const ComponentChildren = suite('Component children tests');
|
||||
describe('Component children', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(ComponentChildren, './fixtures/astro-children');
|
||||
setupBuild(ComponentChildren, './fixtures/astro-children');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-children/',
|
||||
renderers: ['@astrojs/renderer-preact', '@astrojs/renderer-vue', '@astrojs/renderer-svelte'],
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
ComponentChildren('Passes string children to framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/strings');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Passes string children to framework components', async () => {
|
||||
const html = await fixture.fetch('/strings').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
// test 1: Can pass text to Preact components
|
||||
const $preact = $('#preact');
|
||||
expect($preact.text().trim()).toBe('Hello world');
|
||||
|
||||
const $preact = $('#preact');
|
||||
assert.equal($preact.text().trim(), 'Hello world', 'Can pass text to Preact components');
|
||||
// test 2: Can pass text to Vue components
|
||||
const $vue = $('#vue');
|
||||
expect($vue.text().trim()).toBe('Hello world');
|
||||
|
||||
const $vue = $('#vue');
|
||||
assert.equal($vue.text().trim(), 'Hello world', 'Can pass text to Vue components');
|
||||
// test 3: Can pass text to Svelte components
|
||||
const $svelte = $('#svelte');
|
||||
expect($svelte.text().trim()).toBe('Hello world');
|
||||
});
|
||||
|
||||
const $svelte = $('#svelte');
|
||||
assert.equal($svelte.text().trim(), 'Hello world', 'Can pass text to Svelte components');
|
||||
test('Passes markup children to framework components', async () => {
|
||||
const html = await fixture.fetch('/markup').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Can pass markup to Preact components
|
||||
const $preact = $('#preact h1');
|
||||
expect($preact.text().trim()).toBe('Hello world');
|
||||
|
||||
// test 2: Can pass markup to Vue components
|
||||
const $vue = $('#vue h1');
|
||||
expect($vue.text().trim()).toBe('Hello world');
|
||||
|
||||
// test 3: Can pass markup to Svelte components
|
||||
const $svelte = $('#svelte h1');
|
||||
expect($svelte.text().trim()).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('Passes multiple children to framework components', async () => {
|
||||
const html = await fixture.fetch('/multiple').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Can pass multiple children to Preact components
|
||||
const $preact = $('#preact');
|
||||
expect($preact.children()).toHaveLength(2);
|
||||
expect($preact.children(':first-child').text().trim()).toBe('Hello world');
|
||||
expect($preact.children(':last-child').text().trim()).toBe('Goodbye world');
|
||||
|
||||
// test 2: Can pass multiple children to Vue components
|
||||
const $vue = $('#vue');
|
||||
expect($vue.children()).toHaveLength(2);
|
||||
expect($vue.children(':first-child').text().trim()).toBe('Hello world');
|
||||
expect($vue.children(':last-child').text().trim()).toBe('Goodbye world');
|
||||
|
||||
// test 3: Can pass multiple children to Svelte components
|
||||
const $svelte = $('#svelte');
|
||||
expect($svelte.children()).toHaveLength(2);
|
||||
expect($svelte.children(':first-child').text().trim()).toBe('Hello world');
|
||||
expect($svelte.children(':last-child').text().trim()).toBe('Goodbye world');
|
||||
});
|
||||
|
||||
test('Can build a project with component children', async () => {
|
||||
expect(() => fixture.build()).not.toThrow();
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
ComponentChildren('Passes markup children to framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/markup');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const $preact = $('#preact h1');
|
||||
assert.equal($preact.text().trim(), 'Hello world', 'Can pass markup to Preact components');
|
||||
|
||||
const $vue = $('#vue h1');
|
||||
assert.equal($vue.text().trim(), 'Hello world', 'Can pass markup to Vue components');
|
||||
|
||||
const $svelte = $('#svelte h1');
|
||||
assert.equal($svelte.text().trim(), 'Hello world', 'Can pass markup to Svelte components');
|
||||
});
|
||||
|
||||
ComponentChildren('Passes multiple children to framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/multiple');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const $preact = $('#preact');
|
||||
assert.equal($preact.children().length, 2, 'Can pass multiple children to Preact components');
|
||||
assert.equal($preact.children(':first-child').text().trim(), 'Hello world');
|
||||
assert.equal($preact.children(':last-child').text().trim(), 'Goodbye world');
|
||||
|
||||
const $vue = $('#vue');
|
||||
assert.equal($vue.children().length, 2, 'Can pass multiple children to Vue components');
|
||||
assert.equal($vue.children(':first-child').text().trim(), 'Hello world');
|
||||
assert.equal($vue.children(':last-child').text().trim(), 'Goodbye world');
|
||||
|
||||
const $svelte = $('#svelte');
|
||||
assert.equal($svelte.children().length, 2, 'Can pass multiple children to Svelte components');
|
||||
assert.equal($svelte.children(':first-child').text().trim(), 'Hello world');
|
||||
assert.equal($svelte.children(':last-child').text().trim(), 'Goodbye world');
|
||||
});
|
||||
|
||||
ComponentChildren('Can be built', async ({ build }) => {
|
||||
try {
|
||||
await build();
|
||||
assert.ok(true, 'Can build a project with component children');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
assert.ok(false, 'build threw');
|
||||
}
|
||||
});
|
||||
|
||||
ComponentChildren.run();
|
||||
|
|
|
@ -1,44 +1,43 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const ClientOnlyComponents = suite('Client only components tests');
|
||||
describe('Client only components', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(ClientOnlyComponents, './fixtures/astro-client-only');
|
||||
setupBuild(ClientOnlyComponents, './fixtures/astro-client-only');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-client-only/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
ClientOnlyComponents('Loads pages using client:only hydrator', async ({ runtime }) => {
|
||||
let result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Loads pages using client:only hydrator', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.html());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
let html = result.contents;
|
||||
// test 1: <astro-root> is empty
|
||||
expect($('astro-root').html()).toBe('');
|
||||
|
||||
const rootExp = /<astro-root\s[^>]*><\/astro-root>/;
|
||||
assert.ok(rootExp.exec(html), 'astro-root is empty');
|
||||
|
||||
// Grab the svelte import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, svelteRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('renderers/renderer-svelte/client.js')) {
|
||||
svelteRenderer = match[1];
|
||||
// test 2: svelte renderer is on the page
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, svelteRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('renderers/renderer-svelte/client.js')) {
|
||||
svelteRenderer = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(svelteRenderer).toBeTruthy();
|
||||
|
||||
assert.ok(svelteRenderer, 'Svelte renderer is on the page');
|
||||
// test 3: can load svelte renderer
|
||||
result = await fixture.fetch(svelteRenderer);
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
result = await runtime.load(svelteRenderer);
|
||||
assert.equal(result.statusCode, 200, 'Can load svelte renderer');
|
||||
test('Can build a project with svelte dynamic components', async () => {
|
||||
expect(() => fixture.build()).not.toThrow();
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
ClientOnlyComponents('Can be built', async ({ build }) => {
|
||||
try {
|
||||
await build();
|
||||
assert.ok(true, 'Can build a project with svelte dynamic components');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
assert.ok(false, 'build threw');
|
||||
}
|
||||
});
|
||||
|
||||
ClientOnlyComponents.run();
|
||||
|
|
|
@ -1,53 +1,55 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Components = suite('Components tests');
|
||||
describe('Components tests', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Components, './fixtures/astro-components');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-components/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Components('Astro components are able to render framework components', async ({ runtime }) => {
|
||||
let result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Astro components are able to render framework components', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.html());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
// test 1: Renders Astro component
|
||||
const $astro = $('#astro');
|
||||
expect($astro.children()).toHaveLength(3);
|
||||
|
||||
const $astro = $('#astro');
|
||||
assert.equal($astro.children().length, 3, 'Renders astro component');
|
||||
// test 2: Renders React component
|
||||
const $react = $('#react');
|
||||
expect($react).not.toHaveLength(0);
|
||||
|
||||
const $react = $('#react');
|
||||
assert.not.type($react, 'undefined', 'Renders React component');
|
||||
// test 3: Renders Vue component
|
||||
const $vue = $('#vue');
|
||||
expect($vue).not.toHaveLength(0);
|
||||
|
||||
const $vue = $('#vue');
|
||||
assert.not.type($vue, 'undefined', 'Renders Vue component');
|
||||
// test 4: Renders Svelte component
|
||||
const $svelte = $('#svelte');
|
||||
expect($svelte).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
const $svelte = $('#svelte');
|
||||
assert.not.type($svelte, 'undefined', 'Renders Svelte component');
|
||||
test('Allows Components defined in frontmatter', async () => {
|
||||
const html = await fixture.fetch('/frontmatter-component').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('h1')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Still throws an error for undefined components', async () => {
|
||||
const result = await fixture.fetch('/undefined-component');
|
||||
expect(result.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
test('Client attrs not added', async () => {
|
||||
const html = await fixture.fetch('/client').then((res) => res.text());
|
||||
expect(html).not.toEqual(expect.stringMatching(/"client:load": true/));
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
Components('Allows Components defined in frontmatter', async ({ runtime }) => {
|
||||
const result = await runtime.load('/frontmatter-component');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('h1').length, 1);
|
||||
});
|
||||
|
||||
Components('Allows variables named props', async ({ runtime }) => {
|
||||
const result = await runtime.load('/props-shadowing');
|
||||
assert.equal(result.statusCode, 500);
|
||||
});
|
||||
|
||||
Components('Still throws an error for undefined components', async ({ runtime }) => {
|
||||
const result = await runtime.load('/undefined-component');
|
||||
assert.equal(result.statusCode, 500);
|
||||
});
|
||||
|
||||
Components('Svelte component', async ({ runtime }) => {
|
||||
const result = await runtime.load('/client');
|
||||
const html = result.contents;
|
||||
assert.ok(!/"client:load": true/.test(html), 'Client attrs not added');
|
||||
});
|
||||
|
||||
Components.run();
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import cheerio from 'cheerio';
|
||||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setupBuild } from './helpers.js';
|
||||
|
||||
const CSSBundling = suite('CSS Bundling');
|
||||
|
||||
setupBuild(CSSBundling, './fixtures/astro-css-bundling');
|
||||
import { loadFixture } from './test-utils';
|
||||
|
||||
// note: the hashes should be deterministic, but updating the file contents will change hashes
|
||||
// be careful not to test that the HTML simply contains CSS, because it always will! filename and quanity matter here (bundling).
|
||||
|
@ -18,70 +12,75 @@ const EXPECTED_CSS = {
|
|||
};
|
||||
const UNEXPECTED_CSS = ['/_astro/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css'];
|
||||
|
||||
CSSBundling('Bundles CSS', async (context) => {
|
||||
await context.build();
|
||||
describe('CSS Bundling', () => {
|
||||
let fixture;
|
||||
|
||||
const builtCSS = new Set();
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-css-bundling/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
// for all HTML files…
|
||||
for (const [filepath, css] of Object.entries(EXPECTED_CSS)) {
|
||||
const html = await context.readFile(filepath);
|
||||
const $ = cheerio.load(html);
|
||||
test('Bundles CSS', async () => {
|
||||
const builtCSS = new Set();
|
||||
|
||||
// test 1: assert new bundled CSS is present
|
||||
for (const href of css) {
|
||||
const link = $(`link[rel="stylesheet"][href^="${href}"]`);
|
||||
assert.equal(link.length, 1, 'New bundled CSS is not present');
|
||||
builtCSS.add(link.attr('href'));
|
||||
// for all HTML files…
|
||||
for (const [filepath, css] of Object.entries(EXPECTED_CSS)) {
|
||||
const html = await fixture.readFile(filepath);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: assert new bundled CSS is present
|
||||
for (const href of css) {
|
||||
const link = $(`link[rel="stylesheet"][href^="${href}"]`);
|
||||
expect(link).toHaveLength(1);
|
||||
builtCSS.add(link.attr('href'));
|
||||
}
|
||||
|
||||
// test 2: assert old CSS was removed
|
||||
for (const href of UNEXPECTED_CSS) {
|
||||
const link = $(`link[rel="stylesheet"][href="${href}"]`);
|
||||
expect(link).toHaveLength(0);
|
||||
}
|
||||
|
||||
// test 3: preload tags was not removed and attributes was preserved
|
||||
if (filepath === '/preload/index.html') {
|
||||
const stylesheet = $('link[rel="stylesheet"][href^="/_astro/preload/index-"]');
|
||||
const preload = $('link[rel="preload"][href^="/_astro/preload/index-"]');
|
||||
expect(stylesheet[0].attribs.media).toBe('print');
|
||||
expect(preload).toHaveLength(1); // Preload tag was removed
|
||||
}
|
||||
|
||||
// test 4: preload tags was not removed and attributes was preserved
|
||||
if (filepath === '/preload-merge/index.html') {
|
||||
const preload = $('link[rel="preload"]');
|
||||
expect(preload).toHaveLength(1);
|
||||
}
|
||||
|
||||
// test 5: assert all bundled CSS was built and contains CSS
|
||||
for (const url of builtCSS.keys()) {
|
||||
const css = await context.readFile(url);
|
||||
expect(css).toBeTruthy();
|
||||
}
|
||||
|
||||
// test 6: assert ordering is preserved (typography.css before colors.css)
|
||||
const bundledLoc = [...builtCSS].find((k) => k.startsWith('/_astro/common-'));
|
||||
const bundledContents = await context.readFile(bundledLoc);
|
||||
const typographyIndex = bundledContents.indexOf('body{');
|
||||
const colorsIndex = bundledContents.indexOf(':root{');
|
||||
expect(typographyIndex).toBeLessThan(colorsIndex);
|
||||
|
||||
// test 7: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks)
|
||||
const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')];
|
||||
expect(scopedNavStyles).toHaveLength(2);
|
||||
|
||||
// test 8: assert <style global> was not scoped (in Nav.astro)
|
||||
const globalStyles = [...bundledContents.matchAll('html{')];
|
||||
expect(globalStyles).toHaveLength(1);
|
||||
|
||||
// test 9: assert keyframes are only scoped for non-global styles (from Nav.astro)
|
||||
const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')];
|
||||
const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')];
|
||||
expect(scopedKeyframes.length).toBeGreaterThan(0);
|
||||
expect(globalKeyframes.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// test 2: assert old CSS was removed
|
||||
for (const href of UNEXPECTED_CSS) {
|
||||
const link = $(`link[rel="stylesheet"][href="${href}"]`);
|
||||
assert.equal(link.length, 0, 'Old CSS was not removed');
|
||||
}
|
||||
|
||||
// test 3: preload tags was not removed and attributes was preserved
|
||||
if (filepath === '/preload/index.html') {
|
||||
const stylesheet = $('link[rel="stylesheet"][href^="/_astro/preload/index-"]');
|
||||
const preload = $('link[rel="preload"][href^="/_astro/preload/index-"]');
|
||||
assert.equal(stylesheet[0].attribs.media, 'print', 'Attribute was not preserved');
|
||||
assert.equal(preload.length, 1, 'Preload tag was removed');
|
||||
}
|
||||
|
||||
// test 4: preload tags was not removed and attributes was preserved
|
||||
if (filepath === '/preload-merge/index.html') {
|
||||
const preload = $('link[rel="preload"]');
|
||||
assert.equal(preload.length, 1, 'Preload tag was not merged or was removed completly');
|
||||
}
|
||||
}
|
||||
|
||||
// test 5: assert all bundled CSS was built and contains CSS
|
||||
for (const url of builtCSS.keys()) {
|
||||
const css = await context.readFile(url);
|
||||
assert.ok(css, true);
|
||||
}
|
||||
|
||||
// test 6: assert ordering is preserved (typography.css before colors.css)
|
||||
const bundledLoc = [...builtCSS].find((k) => k.startsWith('/_astro/common-'));
|
||||
const bundledContents = await context.readFile(bundledLoc);
|
||||
const typographyIndex = bundledContents.indexOf('body{');
|
||||
const colorsIndex = bundledContents.indexOf(':root{');
|
||||
assert.ok(typographyIndex < colorsIndex);
|
||||
|
||||
// test 7: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks)
|
||||
const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')];
|
||||
assert.is(scopedNavStyles.length, 2);
|
||||
|
||||
// test 8: assert <style global> was not scoped (in Nav.astro)
|
||||
const globalStyles = [...bundledContents.matchAll('html{')];
|
||||
assert.is(globalStyles.length, 1);
|
||||
|
||||
// test 9: assert keyframes are only scoped for non-global styles (from Nav.astro)
|
||||
const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')];
|
||||
const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')];
|
||||
assert.ok(scopedKeyframes.length > 0);
|
||||
assert.ok(globalKeyframes.length > 0);
|
||||
});
|
||||
});
|
||||
|
||||
CSSBundling.run();
|
||||
|
|
|
@ -1,73 +1,77 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const DType = suite('doctype');
|
||||
describe('Doctype', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(DType, './fixtures/astro-doctype');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-doctype/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
DType('Automatically prepends the standards mode doctype', async ({ runtime }) => {
|
||||
const result = await runtime.load('/prepend');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Automatically prepends the standards mode doctype', async () => {
|
||||
const html = await fixture.fetch('/prepend').then((res) => res.text());
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
||||
// test that Doctype always included
|
||||
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
|
||||
});
|
||||
|
||||
test('No attributes added when doctype is provided by user', async () => {
|
||||
const html = await fixture.fetch('/provided').then((res) => res.text());
|
||||
|
||||
// test that Doctype always included
|
||||
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
|
||||
});
|
||||
|
||||
test.skip('Preserves user provided doctype', async () => {
|
||||
const html = await fixture.fetch('/preserve').then((res) => res.text());
|
||||
|
||||
// test that Doctype included was preserved
|
||||
expect(html).toEqual(expect.stringMatching(new RegExp('^<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">')));
|
||||
});
|
||||
|
||||
test('User provided doctype is case insensitive', async () => {
|
||||
const html = await fixture.fetch('/capital').then((res) => res.text());
|
||||
|
||||
// test 1: Doctype left alone
|
||||
expect(html).toEqual(expect.stringMatching(/^<!DOCTYPE html>/));
|
||||
|
||||
// test 2: no closing tag
|
||||
expect(html).not.toEqual(expect.stringContaining('</!DOCTYPE>'));
|
||||
});
|
||||
|
||||
test('Doctype can be provided in a layout', async () => {
|
||||
const html = await fixture.fetch('/in-layout').then((res) => res.text());
|
||||
|
||||
// test 1: doctype is at the front
|
||||
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
|
||||
|
||||
// test 2: A link inside of the head
|
||||
const $ = cheerio.load(html);
|
||||
expect($('head link')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Doctype is added in a layout without one', async () => {
|
||||
const html = await fixture.fetch('/in-layout-no-doctype').then((res) => res.text());
|
||||
|
||||
// test that doctype is at the front
|
||||
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
|
||||
});
|
||||
|
||||
test('Doctype is added in a layout used with markdown pages', async () => {
|
||||
const html = await fixture.fetch('/in-layout-article').then((res) => res.text());
|
||||
|
||||
// test 1: doctype is at the front
|
||||
expect(html).toEqual(expect.stringMatching(/^<!doctype html>/));
|
||||
|
||||
// test 2: A link inside of the head
|
||||
const $ = doc(html);
|
||||
expect($('head link')).toHaveLength(1);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
DType('No attributes added when doctype is provided by user', async ({ runtime }) => {
|
||||
const result = await runtime.load('/provided');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'Doctype always included');
|
||||
});
|
||||
|
||||
DType.skip('Preserves user provided doctype', async ({ runtime }) => {
|
||||
const result = await runtime.load('/preserve');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'), 'Doctype included was preserved');
|
||||
});
|
||||
|
||||
DType('User provided doctype is case insensitive', async ({ runtime }) => {
|
||||
const result = await runtime.load('/capital');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!DOCTYPE html>'), 'Doctype left alone');
|
||||
assert.not.ok(html.includes('</!DOCTYPE>'), 'There should not be a closing tag');
|
||||
});
|
||||
|
||||
DType('Doctype can be provided in a layout', async ({ runtime }) => {
|
||||
const result = await runtime.load('/in-layout');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||
|
||||
const $ = doc(html);
|
||||
assert.equal($('head link').length, 1, 'A link inside of the head');
|
||||
});
|
||||
|
||||
DType('Doctype is added in a layout without one', async ({ runtime }) => {
|
||||
const result = await runtime.load('/in-layout-no-doctype');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||
});
|
||||
|
||||
DType('Doctype is added in a layout used with markdown pages', async ({ runtime }) => {
|
||||
const result = await runtime.load('/in-layout-article');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents.toString('utf-8');
|
||||
assert.ok(html.startsWith('<!doctype html>'), 'doctype is at the front');
|
||||
|
||||
const $ = doc(html);
|
||||
assert.equal($('head link').length, 1, 'A link inside of the head');
|
||||
});
|
||||
|
||||
DType.run();
|
||||
|
|
|
@ -1,72 +1,75 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const DynamicComponents = suite('Dynamic components tests');
|
||||
describe('Dynamic components', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(DynamicComponents, './fixtures/astro-dynamic');
|
||||
setupBuild(DynamicComponents, './fixtures/astro-dynamic');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-dynamic/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
DynamicComponents('Loads client-only packages', async ({ runtime }) => {
|
||||
let result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Loads client-only packages', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
|
||||
// Grab the react-dom import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, reactRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('renderers/renderer-react/client.js')) {
|
||||
reactRenderer = match[1];
|
||||
// Grab the react-dom import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, reactRenderer;
|
||||
while ((match = exp.exec(html))) {
|
||||
if (match[1].includes('renderers/renderer-react/client.js')) {
|
||||
reactRenderer = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(reactRenderer, 'React renderer is on the page');
|
||||
// test 1: React renderer is on the page
|
||||
expect(reactRenderer).toBeTruthy();
|
||||
|
||||
result = await runtime.load(reactRenderer);
|
||||
assert.equal(result.statusCode, 200, 'Can load react renderer');
|
||||
});
|
||||
// test 2: Can load React renderer
|
||||
const result = await fixture.fetch(reactRenderer);
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
DynamicComponents('Loads pages using client:media hydrator', async ({ runtime }) => {
|
||||
let result = await runtime.load('/media');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Loads pages using client:media hydrator', async () => {
|
||||
const html = await fixture.fetch('/media').then((res) => res.text());
|
||||
|
||||
let html = result.contents;
|
||||
assert.ok(html.includes(`value: "(max-width: 700px)"`), 'static value rendered');
|
||||
assert.ok(html.includes(`value: "(max-width: 600px)"`), 'dynamic value rendered');
|
||||
});
|
||||
// test 1: static value rendered
|
||||
expect(html).toEqual(expect.stringContaining(`value: "(max-width: 700px)"`));
|
||||
|
||||
DynamicComponents('Loads pages using client:only hydrator', async ({ runtime }) => {
|
||||
let result = await runtime.load('/client-only');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
// test 2: dynamic value rendered
|
||||
expect(html).toEqual(expect.stringContaining(`value: "(max-width: 600px)"`));
|
||||
});
|
||||
|
||||
let html = result.contents;
|
||||
test('Loads pages using client:only hydrator', async () => {
|
||||
const html = await fixture.fetch('/client-only').then((res) => res.html());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const rootExp = /<astro-root\s[^>]*><\/astro-root>/;
|
||||
assert.ok(rootExp.exec(html), 'astro-root is empty');
|
||||
// test 1: <astro-root> is empty
|
||||
expect($('<astro-root>').html()).toBe('');
|
||||
|
||||
// Grab the svelte import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, svelteRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('renderers/renderer-svelte/client.js')) {
|
||||
svelteRenderer = match[1];
|
||||
// Grab the svelte import
|
||||
const exp = /import\("(.+?)"\)/g;
|
||||
let match, svelteRenderer;
|
||||
while ((match = exp.exec(result.contents))) {
|
||||
if (match[1].includes('renderers/renderer-svelte/client.js')) {
|
||||
svelteRenderer = match[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(svelteRenderer, 'Svelte renderer is on the page');
|
||||
// test 2: Svelte renderer is on the page
|
||||
expect(svelteRenderer).toBeTruthy();
|
||||
|
||||
result = await runtime.load(svelteRenderer);
|
||||
assert.equal(result.statusCode, 200, 'Can load svelte renderer');
|
||||
// test 3: Can load svelte renderer
|
||||
const result = await fixture.fetch(svelteRenderer);
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('Can build a project with svelte dynamic components', async () => {
|
||||
expect(() => fixture.build()).not.toThrow();
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
DynamicComponents('Can be built', async ({ build }) => {
|
||||
try {
|
||||
await build();
|
||||
assert.ok(true, 'Can build a project with svelte dynamic components');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
assert.ok(false, 'build threw');
|
||||
}
|
||||
});
|
||||
|
||||
DynamicComponents.run();
|
||||
|
|
|
@ -1,91 +1,107 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Expressions = suite('Expressions');
|
||||
describe('Expressions', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Expressions, './fixtures/astro-expr');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-expr/',
|
||||
renderers: ['@astrojs/renderer-preact'],
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Expressions('Can load page', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Can load page', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
expect($('#' + col)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
assert.equal($('#' + col).length, 1);
|
||||
}
|
||||
test('Ignores characters inside of strings', async () => {
|
||||
const html = await fixture.fetch('/strings').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
expect($('#' + col)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('Ignores characters inside of line comments', async () => {
|
||||
const html = await fixture.fetch('/line-comments').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
expect($('#' + col)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('Ignores characters inside of multiline comments', async () => {
|
||||
const html = await fixture.fetch('/multiline-comments').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
expect($('#' + col)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('Allows multiple JSX children in mustache', async () => {
|
||||
const html = await fixture.fetch('/multiple-children').then((res) => res.text());
|
||||
|
||||
expect(html).toEqual(expect.stringContaining('#f'));
|
||||
expect(html).not.toEqual(expect.stringContaining('#t'));
|
||||
});
|
||||
|
||||
test('Allows <> Fragments in expressions', async () => {
|
||||
const html = await fixture.fetch('/multiple-children').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#fragment').children()).toHaveLength(3);
|
||||
expect($('#fragment').children('#a')).toHaveLength(1);
|
||||
expect($('#fragment').children('#b')).toHaveLength(1);
|
||||
expect($('#fragment').children('#c')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Does not render falsy values using &&', async () => {
|
||||
const html = await fixture.fetch('/falsy').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Expected {true && <span id="true" />} to render
|
||||
expect($('#true')).toHaveLength(1);
|
||||
|
||||
// test 2: Expected {0 && "VALUE"} to render "0"
|
||||
expect($('#zero').text()).toBe('0');
|
||||
|
||||
// test 3: Expected {false && <span id="false" />} not to render
|
||||
expect($('#false')).toHaveLength(0);
|
||||
|
||||
// test 4: Expected {null && <span id="null" />} not to render
|
||||
expect($('#null')).toHaveLength(0);
|
||||
|
||||
// test 5: Expected {undefined && <span id="undefined" />} not to render
|
||||
expect($('#undefined')).toHaveLength(0);
|
||||
|
||||
// Inside of a component
|
||||
|
||||
// test 6: Expected {true && <span id="true" />} to render
|
||||
expect($('#frag-true')).toHaveLength(1);
|
||||
|
||||
// test 7: Expected {false && <span id="false" />} not to render
|
||||
expect($('#frag-false')).toHaveLength(0);
|
||||
|
||||
// test 8: Expected {null && <span id="null" />} not to render
|
||||
expect($('#frag-null')).toHaveLength(0);
|
||||
|
||||
// test 9: Expected {undefined && <span id="undefined" />} not to render
|
||||
expect($('#frag-undefined')).toHaveLength(0);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
Expressions('Ignores characters inside of strings', async ({ runtime }) => {
|
||||
const result = await runtime.load('/strings');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
assert.equal($('#' + col).length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
Expressions('Ignores characters inside of line comments', async ({ runtime }) => {
|
||||
const result = await runtime.load('/line-comments');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
assert.equal($('#' + col).length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
Expressions('Ignores characters inside of multiline comments', async ({ runtime }) => {
|
||||
const result = await runtime.load('/multiline-comments');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
for (let col of ['red', 'yellow', 'blue']) {
|
||||
assert.equal($('#' + col).length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => {
|
||||
const result = await runtime.load('/multiple-children');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
assert.ok(result.contents.includes('#f') && !result.contents.includes('#t'));
|
||||
});
|
||||
|
||||
Expressions('Allows <> Fragments in expressions', async ({ runtime }) => {
|
||||
const result = await runtime.load('/multiple-children');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#fragment').children().length, 3);
|
||||
assert.equal($('#fragment').children('#a').length, 1);
|
||||
assert.equal($('#fragment').children('#b').length, 1);
|
||||
assert.equal($('#fragment').children('#c').length, 1);
|
||||
});
|
||||
|
||||
Expressions('Does not render falsy values using &&', async ({ runtime }) => {
|
||||
const result = await runtime.load('/falsy');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#true').length, 1, `Expected {true && <span id="true" />} to render`);
|
||||
assert.equal($('#zero').text(), '0', `Expected {0 && "VALUE"} to render "0"`);
|
||||
assert.equal($('#false').length, 0, `Expected {false && <span id="false" />} not to render`);
|
||||
assert.equal($('#null').length, 0, `Expected {null && <span id="null" />} not to render`);
|
||||
assert.equal($('#undefined').length, 0, `Expected {undefined && <span id="undefined" />} not to render`);
|
||||
|
||||
// Inside of a component
|
||||
assert.equal($('#frag-true').length, 1, `Expected {true && <span id="true" />} to render`);
|
||||
assert.equal($('#frag-false').length, 0, `Expected {false && <span id="false" />} not to render`);
|
||||
assert.equal($('#frag-null').length, 0, `Expected {null && <span id="null" />} not to render`);
|
||||
assert.equal($('#frag-undefined').length, 0, `Expected {undefined && <span id="undefined" />} not to render`);
|
||||
});
|
||||
|
||||
Expressions.run();
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Fallback = suite('Dynamic component fallback');
|
||||
describe('Dynamic component fallback', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Fallback, './fixtures/astro-fallback');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-fallback',
|
||||
renderers: ['@astrojs/renderer-preact'],
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Fallback('Shows static content', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Shows static content', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#fallback').text()).toBe('static');
|
||||
});
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#fallback').text(), 'static');
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.close();
|
||||
});
|
||||
});
|
||||
|
||||
Fallback.run();
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import { suite } from 'uvu';
|
||||
import { setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './helpers.js';
|
||||
|
||||
const GetStaticPaths = suite('getStaticPaths()');
|
||||
describe('getStaticPaths()', () => {
|
||||
let fixture;
|
||||
|
||||
setupBuild(GetStaticPaths, './fixtures/astro-get-static-paths');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-get-static-paths/',
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
GetStaticPaths('is only called once during build', async (context) => {
|
||||
// It would throw if this was not true
|
||||
await context.build();
|
||||
test('is only called once during build', async () => {
|
||||
// It would throw if this was not true
|
||||
expect(() => fixture.build()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
GetStaticPaths.run();
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
|
||||
const GlobalBuild = suite('Astro.* built');
|
||||
|
||||
setup(GlobalBuild, './fixtures/astro-global', {
|
||||
runtimeOptions: {
|
||||
mode: 'production',
|
||||
},
|
||||
});
|
||||
|
||||
GlobalBuild('Astro.resolve in the build', async (context) => {
|
||||
const result = await context.runtime.load('/resolve');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('img').attr('src'), '/blog/_astro/src/images/penguin.png');
|
||||
});
|
||||
|
||||
GlobalBuild.run();
|
|
@ -1,54 +1,79 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Global = suite('Astro.*');
|
||||
describe('Astro.*', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Global, './fixtures/astro-global');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-global/',
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Global('Astro.request.url', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#pathname').text(), '/');
|
||||
assert.equal($('#child-pathname').text(), '/');
|
||||
assert.equal($('#nested-child-pathname').text(), '/');
|
||||
beforeAll(async () => {
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
test('Astro.request.url', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#pathname').text()).toBe('/');
|
||||
expect($('#child-pathname').text()).toBe('/');
|
||||
expect($('#nested-child-pathname').text()).toBe('/');
|
||||
});
|
||||
|
||||
test('Astro.request.canonicalURL', async () => {
|
||||
// given a URL, expect the following canonical URL
|
||||
const canonicalURLs = {
|
||||
'/': 'https://mysite.dev/blog/',
|
||||
'/post/post': 'https://mysite.dev/blog/post/post/',
|
||||
'/posts/1': 'https://mysite.dev/blog/posts/',
|
||||
'/posts/2': 'https://mysite.dev/blog/posts/2/',
|
||||
};
|
||||
|
||||
for (const [url, canonicalURL] of Object.entries(canonicalURLs)) {
|
||||
const result = await fixture.fetch(url).then((res) => res.text());
|
||||
const $ = cheerio.load(result.contents);
|
||||
expect($('link[rel="canonical"]').attr('href')).toBe(canonicalURL);
|
||||
}
|
||||
});
|
||||
|
||||
test('Astro.site', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#site').attr('href')).toBe('https://mysite.dev/blog/');
|
||||
});
|
||||
|
||||
test('Astro.resolve in development', async () => {
|
||||
const html = await fixture.fetch('/resolve').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
expect($('img').attr('src')).toBe('/_astro/src/images/penguin.png');
|
||||
expect($('#inner-child img').attr('src')).toBe('/_astro/src/components/nested/images/penguin.png');
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
beforeAll(async () => {
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
test('Astro.resolve in the build', async () => {
|
||||
const html = await fixture.readFile('/resolve/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('img').attr('src')).toBe('/blog/_astro/src/images/penguin.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Global('Astro.request.canonicalURL', async (context) => {
|
||||
// given a URL, expect the following canonical URL
|
||||
const canonicalURLs = {
|
||||
'/': 'https://mysite.dev/blog/',
|
||||
'/post/post': 'https://mysite.dev/blog/post/post/',
|
||||
'/posts/1': 'https://mysite.dev/blog/posts/',
|
||||
'/posts/2': 'https://mysite.dev/blog/posts/2/',
|
||||
};
|
||||
|
||||
for (const [url, canonicalURL] of Object.entries(canonicalURLs)) {
|
||||
const result = await context.runtime.load(url);
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('link[rel="canonical"]').attr('href'), canonicalURL);
|
||||
}
|
||||
});
|
||||
|
||||
Global('Astro.site', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#site').attr('href'), 'https://mysite.dev/blog/');
|
||||
});
|
||||
|
||||
Global('Astro.resolve in development', async (context) => {
|
||||
const result = await context.runtime.load('/resolve');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('img').attr('src'), '/_astro/src/images/penguin.png');
|
||||
assert.equal($('#inner-child img').attr('src'), '/_astro/src/components/nested/images/penguin.png');
|
||||
});
|
||||
|
||||
Global.run();
|
||||
|
|
|
@ -1,52 +1,46 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
/**
|
||||
* note(drew): test was commented-out as this is now handled by Vite. Do we need any tests here?
|
||||
*/
|
||||
|
||||
const HMR = suite('HMR tests');
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
setup(HMR, './fixtures/astro-hmr', {
|
||||
runtimeOptions: {
|
||||
mode: 'development',
|
||||
},
|
||||
describe.skip('HMR tests', () => {
|
||||
let fixture;
|
||||
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-hmr/' });
|
||||
});
|
||||
|
||||
test('Honors the user provided port', async () => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const html = result.contents;
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), "Uses the user's websocket port");
|
||||
});
|
||||
|
||||
test('Does not override script added by the user', async () => {
|
||||
const result = await runtime.load('/manual');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const html = result.contents;
|
||||
assert.ok(/window\.HMR_WEBSOCKET_URL = 'wss:\/\/example.com:3333'/.test(html), "User's script included");
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), 'Ignored when window.HMR_WEBSOCKET_URL set');
|
||||
});
|
||||
|
||||
test('Adds script to static pages too', async () => {
|
||||
const result = await runtime.load('/static');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
|
||||
});
|
||||
|
||||
test("Adds script to pages even if there aren't any elements in the template", async () => {
|
||||
const result = await runtime.load('/no-elements');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
|
||||
});
|
||||
});
|
||||
|
||||
HMR('Honors the user provided port', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), "Uses the user's websocket port");
|
||||
});
|
||||
|
||||
HMR('Does not override script added by the user', async ({ runtime }) => {
|
||||
const result = await runtime.load('/manual');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
|
||||
assert.ok(/window\.HMR_WEBSOCKET_URL = 'wss:\/\/example.com:3333'/.test(html), "User's script included");
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT = 5555/.test(html), 'Ignored when window.HMR_WEBSOCKET_URL set');
|
||||
});
|
||||
|
||||
HMR('Adds script to static pages too', async ({ runtime }) => {
|
||||
const result = await runtime.load('/static');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
|
||||
});
|
||||
|
||||
HMR("Adds script to pages even if there aren't any elements in the template", async ({ runtime }) => {
|
||||
const result = await runtime.load('/no-elements');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('[src="/_snowpack/hmr-client.js"]').length, 1);
|
||||
assert.ok(/window\.HMR_WEBSOCKET_PORT/.test(html), 'websocket port added');
|
||||
});
|
||||
|
||||
HMR.run();
|
||||
|
|
|
@ -1,29 +1,52 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const MarkdownPlugin = suite('Astro Markdown plugin tests');
|
||||
describe('Astro Markdown plugins', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(MarkdownPlugin, './fixtures/astro-markdown-plugins');
|
||||
setupBuild(MarkdownPlugin, './fixtures/astro-markdown-plugins');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-markdown-plugins/',
|
||||
renderers: ['@astrojs/renderer-preact'],
|
||||
markdownOptions: {
|
||||
remarkPlugins: ['remark-code-titles', 'remark-slug', ['rehype-autolink-headings', { behavior: 'prepend' }]],
|
||||
rehypePlugins: [
|
||||
['rehype-toc', { headings: ['h2', 'h3'] }],
|
||||
['rehype-add-classes', { 'h1,h2,h3': 'title' }],
|
||||
],
|
||||
},
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
MarkdownPlugin('Can render markdown with plugins', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Can render markdown with plugins', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('.toc').length, 1, 'Added a TOC');
|
||||
assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1');
|
||||
// test 1: Added a TOC
|
||||
expect($('.toc')).toHaveLength(1);
|
||||
|
||||
// teste 2: Added .title to h1
|
||||
expect($('#hello-world').hasClass('title')).toBeTrue();
|
||||
});
|
||||
|
||||
test('Can render Astro <Markdown> with plugins', async () => {
|
||||
const html = await fixture.fetch('/astro').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Added a TOC
|
||||
expect($('.toc')).toHaveLength(1);
|
||||
|
||||
// teste 2: Added .title to h1
|
||||
expect($('#hello-world').hasClass('title')).toBeTrue();
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
MarkdownPlugin('Can render Astro <Markdown> with plugins', async ({ runtime }) => {
|
||||
const result = await runtime.load('/astro');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('.toc').length, 1, 'Added a TOC');
|
||||
assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1');
|
||||
});
|
||||
|
||||
MarkdownPlugin.run();
|
||||
|
|
|
@ -1,141 +1,180 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Markdown = suite('Astro Markdown tests');
|
||||
describe('Astro Markdown', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Markdown, './fixtures/astro-markdown');
|
||||
setupBuild(Markdown, './fixtures/astro-markdown');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-markdown/',
|
||||
renderers: ['@astrojs/renderer-preact'],
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Markdown('Can load markdown pages with Astro', async ({ runtime }) => {
|
||||
const result = await runtime.load('/post');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.ok($('#first').length, 'There is a div added in markdown');
|
||||
assert.ok($('#test').length, 'There is a div added via a component from markdown');
|
||||
beforeAll(async () => {
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
test('Can load markdown pages with Astro', async () => {
|
||||
const html = await fixture.fetch('/post').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: There is a div added in markdown
|
||||
expect($('#first').length).toBeTruthy();
|
||||
|
||||
// test 2: There is a div added via a component from markdown
|
||||
expect($('#test').length).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Can load more complex jsxy stuff', async () => {
|
||||
const html = await fixture.fetch('/complex').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#test').text()).toBe('Hello world');
|
||||
});
|
||||
|
||||
test('Empty code blocks do not fail', async () => {
|
||||
const html = await fixture.fetch('/empty-code').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: There is not a `<code>` in the codeblock
|
||||
expect($('pre')[0].children).toHaveLength(1);
|
||||
|
||||
// test 2: The empty `<pre>` failed to render
|
||||
expect($('pre')[1].children).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Runs code blocks through syntax highlighter', async () => {
|
||||
const html = await fixture.fetch('/code').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: There are child spans in code blocks
|
||||
expect($('code span').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Scoped styles should not break syntax highlight', async () => {
|
||||
const html = await fixture.fetch('/scopedStyles-code').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: <pre> tag has scopedStyle class passed down
|
||||
expect($('pre').is('[class]')).toBe(true);
|
||||
|
||||
// test 2: <pre> tag has correct language
|
||||
expect($('pre').hasClass('language-js')).toBe(true);
|
||||
|
||||
// test 3: <code> tag has correct language
|
||||
expect($('code').hasClass('language-js')).toBe(true);
|
||||
|
||||
// test 4: There are child spans in code blocks
|
||||
expect($('code span').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Renders correctly when deeply nested on a page', async () => {
|
||||
const html = await fixture.fetch('/deep').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Rendered all children
|
||||
expect($('#deep').children()).toHaveLength(3);
|
||||
|
||||
// tests 2–4: Only rendered title in each section
|
||||
assert.equal($('.a').children()).toHaveLength(1);
|
||||
assert.equal($('.b').children()).toHaveLength(1);
|
||||
assert.equal($('.c').children()).toHaveLength(1);
|
||||
|
||||
// test 5–7: Rendered title in correct section
|
||||
assert.equal($('.a > h2').text()).toBe('A');
|
||||
assert.equal($('.b > h2').text()).toBe('B');
|
||||
assert.equal($('.c > h2').text()).toBe('C');
|
||||
});
|
||||
|
||||
test('Renders recursively', async () => {
|
||||
const html = await fixture.fetch('/recursive').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// tests 1–2: Rendered title correctly
|
||||
expect($('.a > h1').text()).toBe('A');
|
||||
expect($('.b > h1').text()).toBe('B');
|
||||
expect($('.c > h1').text()).toBe('C');
|
||||
});
|
||||
|
||||
test('Renders dynamic content though the content attribute', async () => {
|
||||
const html = await fixture.fetch('/external').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Rendered markdown content
|
||||
expect($('#outer')).toHaveLength(1);
|
||||
|
||||
// test 2: Nested markdown content
|
||||
expect($('#inner')).toHaveLength(1);
|
||||
|
||||
// test 3: Scoped class passed down
|
||||
expect($('#inner').is('[class]')).toBe(true);
|
||||
});
|
||||
|
||||
test('Renders curly braces correctly', async () => {
|
||||
const html = await fixture.fetch('/braces').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Rendered curly braces markdown content
|
||||
expect($('code')).toHaveLength(3);
|
||||
|
||||
// test 2: Rendered curly braces markdown content
|
||||
expect($('code:first-child').text()).toBe('({})');
|
||||
|
||||
// test 3: Rendered curly braces markdown content
|
||||
expect($('code:nth-child(2)').text()).toBe('{...props}');
|
||||
|
||||
// test 4: Rendered curly braces markdown content
|
||||
expect($('code:last-child').text()).toBe('{/* JavaScript */}');
|
||||
});
|
||||
|
||||
test('Does not close parent early when using content attribute (#494)', async () => {
|
||||
const html = await fixture.fetch('/close').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test <Markdown content /> closed div#target early
|
||||
expect($('#target').children()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Can render markdown with --- for horizontal rule', async () => {
|
||||
const result = await fixture.fetch('/dash');
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('Can render markdown content prop (#1259)', async () => {
|
||||
const html = await fixture.fetch('/content').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test Markdown rendered correctly via content prop
|
||||
expect($('h1').text()).toBe('Foo');
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
beforeAll(async () => {
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
test('Bundles client-side JS for prod', async () => {
|
||||
const complexHtml = await fixture.readFile('/complex/index.html');
|
||||
|
||||
// test 1: Counter.js is loaded from page
|
||||
expect(complexHtml).toEqual(expect.stringContaining(`import("/_astro/src/components/Counter.js"`));
|
||||
|
||||
// test 2: Counter.jsx is bundled for prod
|
||||
const counterJs = await fixture.readFile('/_astro/src/components/Counter.js');
|
||||
expect(counterJs).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Markdown('Can load more complex jsxy stuff', async ({ runtime }) => {
|
||||
const result = await runtime.load('/complex');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
const $el = $('#test');
|
||||
assert.equal($el.text(), 'Hello world');
|
||||
});
|
||||
|
||||
Markdown('Runs code blocks through syntax highlighter', async ({ runtime }) => {
|
||||
const result = await runtime.load('/code');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
const $el = $('code span');
|
||||
assert.ok($el.length > 0, 'There are child spans in code blocks');
|
||||
});
|
||||
|
||||
Markdown('Empty code blocks do not fail', async ({ runtime }) => {
|
||||
const result = await runtime.load('/empty-code');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const $el = $('pre');
|
||||
assert.ok($el[0].children.length === 1, 'There is not a `<code>` in the codeblock');
|
||||
assert.ok($el[1].children.length === 0, 'The empty `<pre>` failed to render');
|
||||
});
|
||||
|
||||
Markdown('Scoped styles should not break syntax highlight', async ({ runtime }) => {
|
||||
const result = await runtime.load('/scopedStyles-code');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.ok($('pre').is('[class]'), 'Pre tag has scopedStyle class passed down');
|
||||
assert.ok($('pre').hasClass('language-js'), 'Pre tag has correct language');
|
||||
assert.ok($('code').hasClass('language-js'), 'Code tag has correct language');
|
||||
assert.ok($('code span').length > 0, 'There are child spans in code blocks');
|
||||
});
|
||||
|
||||
Markdown('Bundles client-side JS for prod', async (context) => {
|
||||
await context.build();
|
||||
|
||||
const complexHtml = await context.readFile('/complex/index.html');
|
||||
assert.match(complexHtml, `import("/_astro/src/components/Counter.js"`);
|
||||
|
||||
const counterJs = await context.readFile('/_astro/src/components/Counter.js');
|
||||
assert.ok(counterJs, 'Counter.jsx is bundled for prod');
|
||||
});
|
||||
|
||||
Markdown('Renders correctly when deeply nested on a page', async ({ runtime }) => {
|
||||
const result = await runtime.load('/deep');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#deep').children().length, 3, 'Rendered all children');
|
||||
assert.equal($('.a').children().length, 1, 'Only rendered title in each section');
|
||||
assert.equal($('.b').children().length, 1, 'Only rendered title in each section');
|
||||
assert.equal($('.c').children().length, 1, 'Only rendered title in each section');
|
||||
|
||||
assert.equal($('.a > h2').text(), 'A', 'Rendered title in correct section');
|
||||
assert.equal($('.b > h2').text(), 'B', 'Rendered title in correct section');
|
||||
assert.equal($('.c > h2').text(), 'C', 'Rendered title in correct section');
|
||||
});
|
||||
|
||||
Markdown('Renders recursively', async ({ runtime }) => {
|
||||
const result = await runtime.load('/recursive');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('.a > h1').text(), 'A', 'Rendered title .a correctly');
|
||||
assert.equal($('.b > h1').text(), 'B', 'Rendered title .b correctly');
|
||||
assert.equal($('.c > h1').text(), 'C', 'Rendered title .c correctly');
|
||||
});
|
||||
|
||||
Markdown('Renders dynamic content though the content attribute', async ({ runtime }) => {
|
||||
const result = await runtime.load('/external');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#outer').length, 1, 'Rendered markdown content');
|
||||
assert.equal($('#inner').length, 1, 'Nested markdown content');
|
||||
assert.ok($('#inner').is('[class]'), 'Scoped class passed down');
|
||||
});
|
||||
|
||||
Markdown('Renders curly braces correctly', async ({ runtime }) => {
|
||||
const result = await runtime.load('/braces');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('code').length, 3, 'Rendered curly braces markdown content');
|
||||
assert.equal($('code:first-child').text(), '({})', 'Rendered curly braces markdown content');
|
||||
assert.equal($('code:nth-child(2)').text(), '{...props}', 'Rendered curly braces markdown content');
|
||||
assert.equal($('code:last-child').text(), '{/* JavaScript */}', 'Rendered curly braces markdown content');
|
||||
});
|
||||
|
||||
Markdown('Does not close parent early when using content attribute (#494)', async ({ runtime }) => {
|
||||
const result = await runtime.load('/close');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#target').children().length, 2, '<Markdown content /> closed div#target early');
|
||||
});
|
||||
|
||||
Markdown('Can render markdown with --- for horizontal rule', async ({ runtime }) => {
|
||||
const result = await runtime.load('/dash');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
// It works!
|
||||
});
|
||||
|
||||
Markdown('Can render markdown content prop (#1259)', async ({ runtime }) => {
|
||||
const result = await runtime.load('/content');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('h1').text(), 'Foo', 'Markdown rendered correctly via content prop');
|
||||
|
||||
// It works!
|
||||
});
|
||||
|
||||
Markdown.run();
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const PageDirectoryUrl = suite('pageUrlFormat');
|
||||
describe('pageUrlFormat', () => {
|
||||
let fixture;
|
||||
|
||||
setupBuild(PageDirectoryUrl, './fixtures/astro-page-directory-url');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-page-directory-url',
|
||||
buildOptions: {
|
||||
pageUrlFormat: 'file',
|
||||
},
|
||||
});
|
||||
|
||||
PageDirectoryUrl('outputs', async ({ build, readFile }) => {
|
||||
await build();
|
||||
assert.ok(await readFile('/client.html'));
|
||||
assert.ok(await readFile('/nested-md.html'));
|
||||
assert.ok(await readFile('/nested-astro.html'));
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
test('outputs', async () => {
|
||||
expect(await fixture.readFile('/client.html')).toBeTruthy();
|
||||
expect(await fixture.readFile('/nested-md.html')).toBeTruthy();
|
||||
expect(await fixture.readFile('/nested-astro.html')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
PageDirectoryUrl.run();
|
||||
|
|
|
@ -1,24 +1,43 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Pages = suite('Pages tests');
|
||||
describe('Pages', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Pages, './fixtures/astro-pages');
|
||||
setupBuild(Pages, './fixtures/astro-pages');
|
||||
|
||||
Pages('Can find page with "index" at the end file name', async ({ build, runtime }) => {
|
||||
await build().catch((err) => {
|
||||
assert.ok(!err, 'Error during the build');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-pages/' });
|
||||
});
|
||||
|
||||
const result = await runtime.load('posts/name-with-index');
|
||||
if (result.error) throw new Error(result.error);
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
const $ = doc(result.contents);
|
||||
beforeAll(async () => {
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
assert.equal($('h1').text(), 'Name with index');
|
||||
test('Can find page with "index" at the end file name', async () => {
|
||||
const html = await fixture.fetch('/posts/name-with-index').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).toBe('Name with index');
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
beforeAll(async () => {
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
test('Can find page with "index" at the end file name', async () => {
|
||||
const html = await fixture.readFile('/posts/name-with-index/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('h1').text()).toBe('Name with index');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Pages.run();
|
||||
|
|
|
@ -1,67 +1,54 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Global = suite('Astro.*');
|
||||
describe('Pagination', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Global, './fixtures/astro-pagination');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-pagination/',
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Global('optional root page', async (context) => {
|
||||
{
|
||||
const result = await context.runtime.load('/posts/optional-root-page/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/optional-root-page/2');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/optional-root-page/3');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
test('optional root page', async () => {
|
||||
const results = await Promise.all([fixture.fetch('/posts/optional-root-page/'), fixture.fetch('/posts/optional-root-page/2'), fixture.fetch('/posts/optional-root-page/3')]);
|
||||
for (const result of results) {
|
||||
expect(result.statusCode).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('named root page', async () => {
|
||||
const results = await Promise.all([fixture.fetch('/posts/named-root-page/1'), fixture.fetch('/posts/named-root-page/2'), fixture.fetch('/posts/named-root-page/3')]);
|
||||
for (const result of results) {
|
||||
expect(result.statusCode).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('multiple params', async () => {
|
||||
const params = [
|
||||
{ color: 'red', p: '1' },
|
||||
{ color: 'blue', p: '1' },
|
||||
{ color: 'blue', p: '2' },
|
||||
];
|
||||
await Promise.all(
|
||||
params.map(({ color, p }) => {
|
||||
const html = await fixture.fetch(`/posts/${color}/${p}`).then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#page-a').text()).toBe(p);
|
||||
expect($('#page-b').text()).toBe(p);
|
||||
expect($('#filter').text()).toBe(color);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Global('named root page', async (context) => {
|
||||
{
|
||||
const result = await context.runtime.load('/posts/named-root-page/1');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/named-root-page/2');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/named-root-page/3');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
Global('multiple params', async (context) => {
|
||||
{
|
||||
const result = await context.runtime.load('/posts/red/1');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#page-a').text(), '1');
|
||||
assert.equal($('#page-b').text(), '1');
|
||||
assert.equal($('#filter').text(), 'red');
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/blue/1');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#page-a').text(), '1');
|
||||
assert.equal($('#page-b').text(), '1');
|
||||
assert.equal($('#filter').text(), 'blue');
|
||||
}
|
||||
{
|
||||
const result = await context.runtime.load('/posts/blue/2');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#page-a').text(), '2');
|
||||
assert.equal($('#page-b').text(), '2');
|
||||
assert.equal($('#filter').text(), 'blue');
|
||||
}
|
||||
});
|
||||
|
||||
Global.run();
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Public = suite('Public');
|
||||
describe('Public', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Public, './fixtures/astro-public');
|
||||
setupBuild(Public, './fixtures/astro-public');
|
||||
|
||||
Public('css and js files do not get bundled', async ({ build, readFile }) => {
|
||||
await build().catch((err) => {
|
||||
assert.ok(!err, 'Error during the build');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-public/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
let indexHtml = await readFile('/index.html');
|
||||
assert.ok(indexHtml.includes('<script src="/example.js"></script>'));
|
||||
assert.ok(indexHtml.includes('<link href="/example.css" ref="stylesheet">'));
|
||||
assert.ok(indexHtml.includes('<img src="/images/twitter.png">'));
|
||||
test('css and js files do not get bundled', async () => {
|
||||
let indexHtml = await fixture.readFile('/index.html');
|
||||
expect(indexHtml).toEqual(expect.stringContaining('<script src="/example.js"></script>'));
|
||||
expect(indexHtml).toEqual(expect.stringContaining('<link href="/example.css" ref="stylesheet">'));
|
||||
expect(indexHtml).toEqual(expect.stringContaining('<img src="/images/twitter.png">'));
|
||||
});
|
||||
});
|
||||
|
||||
Public.run();
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const RSS = suite('RSS Generation');
|
||||
describe('RSS Generation', () => {
|
||||
let fixture;
|
||||
|
||||
setupBuild(RSS, './fixtures/astro-rss');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/astro-rss/',
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev',
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
const snapshot = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[MF Doomcast]]></title><description><![CDATA[The podcast about the things you find on a picnic, or at a picnic table]]></description><link>https://mysite.dev/custom/feed.xml</link><language>en-us</language><itunes:author>MF Doom</itunes:author><item><title><![CDATA[Rap Snitch Knishes (feat. Mr. Fantastik)]]></title><link>https://mysite.dev/episode/rap-snitch-knishes/</link><description><![CDATA[Complex named this song the “22nd funniest rap song of all time.”]]></description><pubDate>Tue, 16 Nov 2004 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>172</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Fazers]]></title><link>https://mysite.dev/episode/fazers/</link><description><![CDATA[Rhapsody ranked Take Me to Your Leader 17th on its list “Hip-Hop’s Best Albums of the Decade”]]></description><pubDate>Thu, 03 Jul 2003 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>197</itunes:duration><itunes:explicit>true</itunes:explicit></item><item><title><![CDATA[Rhymes Like Dimes (feat. Cucumber Slice)]]></title><link>https://mysite.dev/episode/rhymes-like-dimes/</link><description><![CDATA[Operation: Doomsday has been heralded as an underground classic that established MF Doom's rank within the underground hip-hop scene during the early to mid-2000s.\n]]></description><pubDate>Tue, 19 Oct 1999 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>259</itunes:duration><itunes:explicit>true</itunes:explicit></item></channel></rss>`;
|
||||
|
||||
RSS('Generates RSS correctly', async (context) => {
|
||||
await context.build();
|
||||
let rss = await context.readFile('/custom/feed.xml');
|
||||
assert.match(rss, snapshot);
|
||||
it('generates RSS correctly', async () => {
|
||||
const rss = await fixture.readFile('/custom/feed.xml');
|
||||
expect(rss).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
RSS.run();
|
||||
|
|
|
@ -1,46 +1,44 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { scopeRule } from '#astro/compiler';
|
||||
/**
|
||||
* note(drew): TODO: update this test for compiler
|
||||
*/
|
||||
|
||||
const ScopedStyles = suite('Astro PostCSS Scoped Styles Plugin');
|
||||
// import { scopeRule } from '../dist/compiler';
|
||||
|
||||
const className = 'astro-abcd1234';
|
||||
|
||||
ScopedStyles('Scopes rules correctly', () => {
|
||||
// Note: assume all selectors have no unnecessary spaces (i.e. must be minified)
|
||||
const tests = {
|
||||
'.class': `.class.${className}`,
|
||||
h1: `h1.${className}`,
|
||||
'.nav h1': `.nav.${className} h1.${className}`,
|
||||
'.class+.class': `.class.${className}+.class.${className}`,
|
||||
'.class~:global(a)': `.class.${className}~a`,
|
||||
'.class *': `.class.${className} .${className}`,
|
||||
'.class>*': `.class.${className}>.${className}`,
|
||||
'.class button:focus': `.class.${className} button.${className}:focus`,
|
||||
'.class h3::before': `.class.${className} h3.${className}::before`,
|
||||
'button:focus::before': `button.${className}:focus::before`,
|
||||
'.class :global(*)': `.class.${className} *`,
|
||||
'.class :global(.nav:not(.is-active))': `.class.${className} .nav:not(.is-active)`, // preserve nested parens
|
||||
'.class :global(ul li)': `.class.${className} ul li`, // allow doubly-scoped selectors
|
||||
':global(body:not(.is-light)).is-dark,:global(body:not(.is-dark)).is-light': `body:not(.is-light).is-dark,body:not(.is-dark).is-light`, // :global() can contain parens, and can be chained off of
|
||||
':global(.foo):global(.bar)': '.foo.bar', // more :global() shenanigans
|
||||
'.class:global(.bar)': `.class.bar`, // this is technically a “useless“ :global() but it should still be extracted
|
||||
'.class:not(.is-active):not(.is-disabled)': `.class.${className}:not(.is-active):not(.is-disabled)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then it‘s worth a discussion
|
||||
':hover.a:focus': `.${className}:hover.a:focus`, // weird but still valid (yes, it’s valid)
|
||||
'*:hover': `.${className}:hover`,
|
||||
':not(.is-disabled).a': `.${className}:not(.is-disabled).a`, // also valid
|
||||
'body h1': `body h1.${className}`, // body shouldn‘t be scoped; it‘s not a component
|
||||
'html,body': `html,body`,
|
||||
from: 'from', // ignore keyframe keywords (below)
|
||||
to: 'to',
|
||||
'55%': '55%',
|
||||
'.class\\:class': `.class\\:class.${className}`, // classes can contain special characters if escaped
|
||||
'.class\\:class:focus': `.class\\:class.${className}:focus`,
|
||||
};
|
||||
|
||||
for (const [given, expected] of Object.entries(tests)) {
|
||||
assert.equal(scopeRule(given, className), expected);
|
||||
}
|
||||
describe.skip('Scoped styles', () => {
|
||||
// test('Scopes rules correctly', () => {
|
||||
// const className = 'astro-abcd1234';
|
||||
// // Note: assume all selectors have no unnecessary spaces (i.e. must be minified)
|
||||
// const tests = {
|
||||
// '.class': `.class.${className}`,
|
||||
// h1: `h1.${className}`,
|
||||
// '.nav h1': `.nav.${className} h1.${className}`,
|
||||
// '.class+.class': `.class.${className}+.class.${className}`,
|
||||
// '.class~:global(a)': `.class.${className}~a`,
|
||||
// '.class *': `.class.${className} .${className}`,
|
||||
// '.class>*': `.class.${className}>.${className}`,
|
||||
// '.class button:focus': `.class.${className} button.${className}:focus`,
|
||||
// '.class h3::before': `.class.${className} h3.${className}::before`,
|
||||
// 'button:focus::before': `button.${className}:focus::before`,
|
||||
// '.class :global(*)': `.class.${className} *`,
|
||||
// '.class :global(.nav:not(.is-active))': `.class.${className} .nav:not(.is-active)`, // preserve nested parens
|
||||
// '.class :global(ul li)': `.class.${className} ul li`, // allow doubly-scoped selectors
|
||||
// ':global(body:not(.is-light)).is-dark,:global(body:not(.is-dark)).is-light': `body:not(.is-light).is-dark,body:not(.is-dark).is-light`, // :global() can contain parens, and can be chained off of
|
||||
// ':global(.foo):global(.bar)': '.foo.bar', // more :global() shenanigans
|
||||
// '.class:global(.bar)': `.class.bar`, // this is technically a “useless“ :global() but it should still be extracted
|
||||
// '.class:not(.is-active):not(.is-disabled)': `.class.${className}:not(.is-active):not(.is-disabled)`, // Note: the :not() selector can NOT contain multiple classes, so this is correct; if this causes issues for some people then it‘s worth a discussion
|
||||
// ':hover.a:focus': `.${className}:hover.a:focus`, // weird but still valid (yes, it’s valid)
|
||||
// '*:hover': `.${className}:hover`,
|
||||
// ':not(.is-disabled).a': `.${className}:not(.is-disabled).a`, // also valid
|
||||
// 'body h1': `body h1.${className}`, // body shouldn‘t be scoped; it‘s not a component
|
||||
// 'html,body': `html,body`,
|
||||
// from: 'from', // ignore keyframe keywords (below)
|
||||
// to: 'to',
|
||||
// '55%': '55%',
|
||||
// '.class\\:class': `.class\\:class.${className}`, // classes can contain special characters if escaped
|
||||
// '.class\\:class:focus': `.class\\:class.${className}:focus`,
|
||||
// };
|
||||
// for (const [given, expected] of Object.entries(tests)) {
|
||||
// expect(scopeRule(given, className)).toBe(expected);
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
ScopedStyles.run();
|
||||
|
|
|
@ -1,62 +1,79 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import { doc } from './test-utils.js';
|
||||
import cheerio from 'cheerio';
|
||||
import path from 'path';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Scripts = suite('Hoisted scripts');
|
||||
describe('Hoisted scripts', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Scripts, './fixtures/astro-scripts');
|
||||
setupBuild(Scripts, './fixtures/astro-scripts');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-scripts/' });
|
||||
});
|
||||
|
||||
Scripts('Moves external scripts up', async ({ runtime }) => {
|
||||
const result = await runtime.load('/external');
|
||||
if (result.error) throw new Error(result.error);
|
||||
assert.equal(result.statusCode, 200);
|
||||
const html = result.contents;
|
||||
describe('dev', () => {
|
||||
let devServer;
|
||||
|
||||
const $ = doc(html);
|
||||
assert.equal($('head script[type="module"][data-astro="hoist"]').length, 2);
|
||||
assert.equal($('body script').length, 0);
|
||||
beforeAll(async () => {
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
test('Moves external scripts up', async () => {
|
||||
const html = await fixture.fetch('/external').then((res) => res.text());
|
||||
const $ = doc(html);
|
||||
|
||||
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(2);
|
||||
expect($('body script')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Moves inline scripts up', async () => {
|
||||
const html = await fixture.fetch('/inline').then((res) => res.text());
|
||||
const $ = doc(html);
|
||||
|
||||
expect($('head script[type="module"][data-astro="hoist"]')).toHaveLength(1);
|
||||
expect($('body script')).toHaveLength(0);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
beforeAll(async () => {
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
test('Inline page builds the scripts to a single bundle', async () => {
|
||||
/* Inline page */
|
||||
let inline = await fixture.readFile('/inline/index.html');
|
||||
let $ = cheerio.load(inline);
|
||||
|
||||
// test 1: Just one entry module
|
||||
assert.equal($('script')).toHaveLength(1);
|
||||
|
||||
// test 2: attr removed
|
||||
expect($('script').attr('data-astro')).toBe(undefined);
|
||||
|
||||
let entryURL = path.join('inline', $('script').attr('src'));
|
||||
let inlineEntryJS = await fixture.readFile(entryURL);
|
||||
|
||||
// test 3: the JS exists
|
||||
expect(inlineEntryJS).toBeTruthy();
|
||||
});
|
||||
|
||||
test('External page builds the scripts to a single bundle', async () => {
|
||||
let external = await fixture.readFile('/external/index.html');
|
||||
$ = doc(external);
|
||||
|
||||
// test 1: there are two scripts
|
||||
assert.equal($('script')).toHaveLength(2);
|
||||
|
||||
let el = $('script').get(1);
|
||||
entryURL = path.join('external', $(el).attr('src'));
|
||||
let externalEntryJS = await readFile(entryURL);
|
||||
|
||||
// test 2: the JS exists
|
||||
expect(externalEntryJS).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Scripts('Moves inline scripts up', async ({ runtime }) => {
|
||||
const result = await runtime.load('/inline');
|
||||
if (result.error) throw new Error(result.error);
|
||||
assert.equal(result.statusCode, 200);
|
||||
const html = result.contents;
|
||||
|
||||
const $ = doc(html);
|
||||
assert.equal($('head script[type="module"][data-astro="hoist"]').length, 1);
|
||||
assert.equal($('body script').length, 0);
|
||||
});
|
||||
|
||||
Scripts('Builds the scripts to a single bundle', async ({ build, readFile }) => {
|
||||
try {
|
||||
await build();
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
assert.ok(!err);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Inline page */
|
||||
let inline = await readFile('/inline/index.html');
|
||||
let $ = doc(inline);
|
||||
assert.equal($('script').length, 1, 'Just one entry module');
|
||||
assert.equal($('script').attr('data-astro'), undefined, 'attr removed');
|
||||
let entryURL = path.join('inline', $('script').attr('src'));
|
||||
let inlineEntryJS = await readFile(entryURL);
|
||||
assert.ok(inlineEntryJS, 'The JS exists');
|
||||
|
||||
/* External page */
|
||||
let external = await readFile('/external/index.html');
|
||||
$ = doc(external);
|
||||
assert.equal($('script').length, 2, 'There are two scripts');
|
||||
let el = $('script').get(1);
|
||||
entryURL = path.join('external', $(el).attr('src'));
|
||||
let externalEntryJS = await readFile(entryURL);
|
||||
assert.ok(externalEntryJS, 'got JS');
|
||||
});
|
||||
|
||||
Scripts.run();
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setup } from './helpers.js';
|
||||
|
||||
const Search = suite('Search paths');
|
||||
|
||||
setup(Search, './fixtures/astro-basic');
|
||||
|
||||
Search('Finds the root page', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.equal(result.statusCode, 200);
|
||||
});
|
||||
|
||||
Search('Matches pathname to filename', async ({ runtime }) => {
|
||||
assert.equal((await runtime.load('/news')).statusCode, 200);
|
||||
assert.equal((await runtime.load('/news/')).statusCode, 200);
|
||||
});
|
||||
|
||||
Search('Matches pathname to a nested index.astro file', async ({ runtime }) => {
|
||||
assert.equal((await runtime.load('/nested-astro')).statusCode, 200);
|
||||
assert.equal((await runtime.load('/nested-astro/')).statusCode, 200);
|
||||
});
|
||||
|
||||
Search('Matches pathname to a nested index.md file', async ({ runtime }) => {
|
||||
assert.equal((await runtime.load('/nested-md')).statusCode, 200);
|
||||
assert.equal((await runtime.load('/nested-md/')).statusCode, 200);
|
||||
});
|
||||
|
||||
Search.run();
|
|
@ -1,17 +1,15 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Sitemap = suite('Sitemap Generation');
|
||||
describe('Sitemap Generation', () => {
|
||||
let fixture;
|
||||
|
||||
setupBuild(Sitemap, './fixtures/astro-rss');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-rss/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
const snapshot = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://mysite.dev/episode/fazers/</loc></url><url><loc>https://mysite.dev/episode/rap-snitch-knishes/</loc></url><url><loc>https://mysite.dev/episode/rhymes-like-dimes/</loc></url><url><loc>https://mysite.dev/episodes/</loc></url></urlset>\n`;
|
||||
|
||||
Sitemap('Generates Sitemap correctly', async (context) => {
|
||||
await context.build();
|
||||
let sitemap = await context.readFile('/sitemap.xml');
|
||||
assert.match(sitemap, snapshot);
|
||||
test('Generates Sitemap correctly', async () => {
|
||||
let sitemap = await fixture.readFile('/sitemap.xml');
|
||||
expect(sitemap).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
Sitemap.run();
|
||||
|
|
|
@ -1,133 +1,120 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Slots = suite('Slot test');
|
||||
describe('Slots', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Slots, './fixtures/astro-slots', {
|
||||
runtimeOptions: {
|
||||
mode: 'development',
|
||||
},
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-slots/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
test('Basic named slots work', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a').text()).toBe('A');
|
||||
expect($('#b').text()).toBe('B');
|
||||
expect($('#c').text()).toBe('C');
|
||||
expect($('#default').text()).toBe('Default');
|
||||
});
|
||||
|
||||
test('Dynamic named slots work', async () => {
|
||||
const html = await fixture.fetch('/dynamic').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a').text()).toBe('A');
|
||||
expect($('#b').text()).toBe('B');
|
||||
expect($('#c').text()).toBe('C');
|
||||
expect($('#default').text()).toBe('Default');
|
||||
});
|
||||
|
||||
test('Slots render fallback content by default', async () => {
|
||||
const html = await fixture.fetch('/fallback').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#default')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Slots override fallback content', async () => {
|
||||
const html = await fixture.fetch('/fallback-override').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#override')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Slots work with multiple elements', async () => {
|
||||
const html = await fixture.fetch('/multiple').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a').text()).toBe('ABC');
|
||||
});
|
||||
|
||||
test('Slots work on Components', async () => {
|
||||
const html = await fixture.fetch('/component').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: #a renders
|
||||
expect($('#a')).toHaveLength(1);
|
||||
|
||||
// test 2: Slotted component into #a
|
||||
expect($('#a').children('astro-component')).toHaveLength(1);
|
||||
|
||||
// test 3: Slotted component into default slot
|
||||
expect($('#default').children('astro-component')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Slots API work on Components', async () => {
|
||||
// IDs will exist whether the slots are filled or not
|
||||
{
|
||||
const html = await fixture.fetch('/slottedapi-default').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a')).toHaveLength(1);
|
||||
expect($('#b')).toHaveLength(1);
|
||||
expect($('#c')).toHaveLength(1);
|
||||
expect($('#default')).toHaveLength(1);
|
||||
}
|
||||
|
||||
// IDs will not exist because the slots are not filled
|
||||
{
|
||||
const html = await fixture.fetch('/slottedapi-empty').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a')).toHaveLength(0);
|
||||
expect($('#b')).toHaveLength(0);
|
||||
expect($('#c')).toHaveLength(0);
|
||||
expect($('#default')).toHaveLength(0);
|
||||
}
|
||||
|
||||
// IDs will exist because the slots are filled
|
||||
{
|
||||
const html = await fixture.fetch('/slottedapi-filled').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a')).toHaveLength(1);
|
||||
expect($('#b')).toHaveLength(1);
|
||||
expect($('#c')).toHaveLength(1);
|
||||
|
||||
expect($('#default')).toHaveLength(0); // the default slot is not filled
|
||||
}
|
||||
|
||||
// Default ID will exist because the default slot is filled
|
||||
{
|
||||
const html = await fixture.fetch('/slottedapi-default-filled').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('#a')).toHaveLength(0);
|
||||
expect($('#b')).toHaveLength(0);
|
||||
expect($('#c')).toHaveLength(0);
|
||||
|
||||
expect($('#default')).toHaveLength(1); // the default slot is filled
|
||||
}
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
setupBuild(Slots, './fixtures/astro-slots');
|
||||
|
||||
Slots('Basic named slots work', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').text(), 'A');
|
||||
assert.equal($('#b').text(), 'B');
|
||||
assert.equal($('#c').text(), 'C');
|
||||
assert.equal($('#default').text(), 'Default');
|
||||
});
|
||||
|
||||
Slots('Dynamic named slots work', async ({ runtime }) => {
|
||||
const result = await runtime.load('/dynamic');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').text(), 'A');
|
||||
assert.equal($('#b').text(), 'B');
|
||||
assert.equal($('#c').text(), 'C');
|
||||
assert.equal($('#default').text(), 'Default');
|
||||
});
|
||||
|
||||
Slots('Slots render fallback content by default', async ({ runtime }) => {
|
||||
const result = await runtime.load('/fallback');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#default').length, 1);
|
||||
});
|
||||
|
||||
Slots('Slots override fallback content', async ({ runtime }) => {
|
||||
const result = await runtime.load('/fallback-override');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#override').length, 1);
|
||||
});
|
||||
|
||||
Slots('Slots work with multiple elements', async ({ runtime }) => {
|
||||
const result = await runtime.load('/multiple');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').text(), 'ABC');
|
||||
});
|
||||
|
||||
Slots('Slots work on Components', async ({ runtime }) => {
|
||||
const result = await runtime.load('/component');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').length, 1);
|
||||
assert.equal($('#a').children('astro-component').length, 1, 'Slotted component into #a');
|
||||
assert.equal($('#default').children('astro-component').length, 1, 'Slotted component into default slot');
|
||||
});
|
||||
|
||||
Slots('Slots API work on Components', async ({ runtime }) => {
|
||||
// IDs will exist whether the slots are filled or not
|
||||
{
|
||||
const result = await runtime.load('/slottedapi-default');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').length, 1);
|
||||
assert.equal($('#b').length, 1);
|
||||
assert.equal($('#c').length, 1);
|
||||
assert.equal($('#default').length, 1);
|
||||
}
|
||||
|
||||
// IDs will not exist because the slots are not filled
|
||||
{
|
||||
const result = await runtime.load('/slottedapi-empty');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').length, 0);
|
||||
assert.equal($('#b').length, 0);
|
||||
assert.equal($('#c').length, 0);
|
||||
assert.equal($('#default').length, 0);
|
||||
}
|
||||
|
||||
// IDs will exist because the slots are filled
|
||||
{
|
||||
const result = await runtime.load('/slottedapi-filled');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').length, 1);
|
||||
assert.equal($('#b').length, 1);
|
||||
assert.equal($('#c').length, 1);
|
||||
|
||||
assert.equal($('#default').length, 0); // the default slot is not filled
|
||||
}
|
||||
|
||||
// Default ID will exist because the default slot is filled
|
||||
{
|
||||
const result = await runtime.load('/slottedapi-default-filled');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.equal($('#a').length, 0);
|
||||
assert.equal($('#b').length, 0);
|
||||
assert.equal($('#c').length, 0);
|
||||
|
||||
assert.equal($('#default').length, 1); // the default slot is filled
|
||||
}
|
||||
});
|
||||
|
||||
Slots.run();
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
|
||||
const StylesSSR = suite('Styles SSR');
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
/** Basic CSS minification; removes some flakiness in testing CSS */
|
||||
function cssMinify(css) {
|
||||
|
@ -15,136 +11,148 @@ function cssMinify(css) {
|
|||
.replace(/;}/g, '}'); // collapse block
|
||||
}
|
||||
|
||||
setup(StylesSSR, './fixtures/astro-styles-ssr');
|
||||
describe('Styles SSR', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
StylesSSR('Has <link> tags', async ({ runtime }) => {
|
||||
const MUST_HAVE_LINK_TAGS = [
|
||||
'/_astro/src/components/ReactCSS.css',
|
||||
'/_astro/src/components/ReactModules.module.css',
|
||||
'/_astro/src/components/SvelteScoped.svelte.css',
|
||||
'/_astro/src/components/VueCSS.vue.css',
|
||||
'/_astro/src/components/VueModules.vue.css',
|
||||
'/_astro/src/components/VueScoped.vue.css',
|
||||
];
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
test('Has <link> tags', async () => {
|
||||
const MUST_HAVE_LINK_TAGS = [
|
||||
'/src/components/ReactCSS.css',
|
||||
'/src/components/ReactModules.module.css',
|
||||
'/src/components/SvelteScoped.css',
|
||||
'/src/components/VueCSS.css',
|
||||
'/src/components/VueModules.css',
|
||||
'/src/components/VueScoped.css',
|
||||
];
|
||||
|
||||
for (const href of MUST_HAVE_LINK_TAGS) {
|
||||
const el = $(`link[href="${href}"]`);
|
||||
assert.equal(el.length, 1);
|
||||
}
|
||||
});
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
StylesSSR('Has correct CSS classes', async ({ runtime }) => {
|
||||
// TODO: remove this (temporary CI patch)
|
||||
if (process.version.startsWith('v14.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const MUST_HAVE_CLASSES = {
|
||||
'#react-css': 'react-title',
|
||||
'#react-modules': 'title', // ⚠️ this should be transformed
|
||||
'#vue-css': 'vue-title',
|
||||
'#vue-modules': 'title', // ⚠️ this should also be transformed
|
||||
'#vue-scoped': 'vue-title', // also has data-v-* property
|
||||
'#svelte-scoped': 'svelte-title', // also has additional class
|
||||
};
|
||||
|
||||
for (const [selector, className] of Object.entries(MUST_HAVE_CLASSES)) {
|
||||
const el = $(selector);
|
||||
if (selector === '#react-modules' || selector === '#vue-modules') {
|
||||
// this will generate differently on Unix vs Windows. Here we simply test that it has transformed
|
||||
assert.match(el.attr('class'), new RegExp(`^_${className}_[A-Za-z0-9-_]+`)); // className should be transformed, surrounded by underscores and other stuff
|
||||
} else {
|
||||
// if this is not a CSS module, it should remain as expected
|
||||
assert.ok(el.attr('class').includes(className));
|
||||
for (const href of MUST_HAVE_LINK_TAGS) {
|
||||
const el = $(`link[href="${href}"]`);
|
||||
expect(el).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
// add’l test: Vue Scoped styles should have data-v-* attribute
|
||||
if (selector === '#vue-scoped') {
|
||||
const { attribs } = el.get(0);
|
||||
const scopeId = Object.keys(attribs).find((k) => k.startsWith('data-v-'));
|
||||
assert.ok(scopeId);
|
||||
test('Has correct CSS classes', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const MUST_HAVE_CLASSES = {
|
||||
'#react-css': 'react-title',
|
||||
'#react-modules': 'title', // ⚠️ this should be transformed
|
||||
'#vue-css': 'vue-title',
|
||||
'#vue-modules': 'title', // ⚠️ this should also be transformed
|
||||
'#vue-scoped': 'vue-title', // also has data-v-* property
|
||||
'#svelte-scoped': 'svelte-title', // also has additional class
|
||||
};
|
||||
|
||||
for (const [selector, className] of Object.entries(MUST_HAVE_CLASSES)) {
|
||||
const el = $(selector);
|
||||
if (selector === '#react-modules' || selector === '#vue-modules') {
|
||||
// this will generate differently on Unix vs Windows. Here we simply test that it has transformed
|
||||
expect(el.attr('class')).toEqual(expect.stringMatching(new RegExp(`^_${className}_[A-Za-z0-9-_]+`))); // className should be transformed, surrounded by underscores and other stuff
|
||||
} else {
|
||||
// if this is not a CSS module, it should remain as expected
|
||||
expect(el.attr('class')).toEqual(expect.stringContaining(className));
|
||||
}
|
||||
|
||||
// add’l test: Vue Scoped styles should have data-v-* attribute
|
||||
if (selector === '#vue-scoped') {
|
||||
const { attribs } = el.get(0);
|
||||
const scopeId = Object.keys(attribs).find((k) => k.startsWith('data-v-'));
|
||||
expect(scopeId).toBeTruthy();
|
||||
}
|
||||
|
||||
// add’l test: Svelte should have another class
|
||||
if (selector === '#svelte-title') {
|
||||
expect(el.attr('class')).not.toBe(className);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add’l test: Svelte should have another class
|
||||
if (selector === '#svelte-title') {
|
||||
assert.not.equal(el.attr('class'), className);
|
||||
}
|
||||
}
|
||||
});
|
||||
test('CSS Module support in .astro', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
StylesSSR('CSS Module support in .astro', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
let scopedClass;
|
||||
|
||||
let scopedClass;
|
||||
// test 1: <style> tag in <head> is transformed
|
||||
const css = cssMinify(
|
||||
$('style')
|
||||
.html()
|
||||
.replace(/\.astro-[A-Za-z0-9-]+/, (match) => {
|
||||
scopedClass = match; // get class hash from result
|
||||
return match;
|
||||
})
|
||||
);
|
||||
|
||||
// test 1: <style> tag in <head> is transformed
|
||||
const css = cssMinify(
|
||||
expect(css).toBe(`.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`);
|
||||
|
||||
// test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly)
|
||||
const wrapper = $(`.wrapper${scopedClass}`);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Astro scoped styles', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const el1 = $('#dynamic-class');
|
||||
const el2 = $('#dynamic-vis');
|
||||
|
||||
let scopedClass;
|
||||
|
||||
$('#class')
|
||||
.attr('class')
|
||||
.replace(/astro-[A-Za-z0-9-]+/, (match) => {
|
||||
scopedClass = match;
|
||||
return match;
|
||||
});
|
||||
|
||||
// test 1: Astro component missing scoped class
|
||||
expect(scopedClass).toBe(``);
|
||||
|
||||
// test 2–3: children get scoped class
|
||||
expect(el1.attr('class')).toBe(`blue ${scopedClass}`);
|
||||
expect(el2.attr('class')).toBe(`visible ${scopedClass}`);
|
||||
|
||||
const { contents: css } = await fixture.fetch('/src/components/Astro.astro.css').then((res) => res.text());
|
||||
|
||||
// test 4: CSS generates as expected
|
||||
expect(cssMinify(css.toString())).toBe(`.blue.${scopedClass}{color:powderblue}.color\\:blue.${scopedClass}{color:powderblue}.visible.${scopedClass}{display:block}`);
|
||||
});
|
||||
|
||||
test('Astro scoped styles skipped without <style>', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Astro component without <style> should not include scoped class
|
||||
expect($('#no-scope').attr('class')).toBe(undefined);
|
||||
});
|
||||
|
||||
test('Astro scoped styles can be passed to child components', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
let scopedClass;
|
||||
$('style')
|
||||
.html()
|
||||
.replace(/\.astro-[A-Za-z0-9-]+/, (match) => {
|
||||
scopedClass = match; // get class hash from result
|
||||
.replace(/outer\.(astro-[A-Za-z0-9-]+)/, (match, p1) => {
|
||||
scopedClass = p1;
|
||||
return match;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
assert.match(css, `.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`);
|
||||
expect($('#passed-in').attr('class')).toBe(`outer ${scopedClass}`);
|
||||
});
|
||||
|
||||
// test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly)
|
||||
const wrapper = $(`.wrapper${scopedClass}`);
|
||||
assert.equal(wrapper.length, 1);
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
StylesSSR('Astro scoped styles', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
|
||||
const el1 = $('#dynamic-class');
|
||||
const el2 = $('#dynamic-vis');
|
||||
|
||||
let scopedClass;
|
||||
|
||||
$('#class')
|
||||
.attr('class')
|
||||
.replace(/astro-[A-Za-z0-9-]+/, (match) => {
|
||||
scopedClass = match;
|
||||
return match;
|
||||
});
|
||||
|
||||
assert.ok(scopedClass, `Astro component missing scoped class`);
|
||||
assert.match(el1.attr('class'), `blue ${scopedClass}`);
|
||||
assert.match(el2.attr('class'), `visible ${scopedClass}`);
|
||||
|
||||
const { contents: css } = await runtime.load('/_astro/src/components/Astro.astro.css');
|
||||
assert.match(cssMinify(css.toString()), `.blue.${scopedClass}{color:powderblue}.color\\:blue.${scopedClass}{color:powderblue}.visible.${scopedClass}{display:block}`);
|
||||
});
|
||||
|
||||
StylesSSR('Astro scoped styles skipped without <style>', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
|
||||
assert.type($('#no-scope').attr('class'), 'undefined', `Astro component without <style> should not include scoped class`);
|
||||
});
|
||||
|
||||
StylesSSR('Astro scoped styles can be passed to child components', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
const $ = doc(result.contents);
|
||||
|
||||
let scopedClass;
|
||||
$('style')
|
||||
.html()
|
||||
.replace(/outer\.(astro-[A-Za-z0-9-]+)/, (match, p1) => {
|
||||
scopedClass = p1;
|
||||
return match;
|
||||
});
|
||||
|
||||
assert.match($('#passed-in').attr('class'), `outer ${scopedClass}`);
|
||||
});
|
||||
|
||||
StylesSSR.run();
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup, setupBuild } from './helpers.js';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Throwable = suite('Throw test');
|
||||
describe('Throw', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Throwable, './fixtures/astro-throw', {
|
||||
runtimeOptions: {
|
||||
mode: 'development',
|
||||
},
|
||||
});
|
||||
setupBuild(Throwable, './fixtures/astro-throw');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-throw/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Throwable('Can throw an error from an `.astro` file', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.equal(result.statusCode, 500);
|
||||
assert.equal(result.error.message, 'Oops!');
|
||||
});
|
||||
test('Can throw an error from an `.astro` file', async () => {
|
||||
const result = await fixture.fetch('/');
|
||||
expect(result.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
Throwable('Does not complete build when Error is thrown', async ({ build }) => {
|
||||
await build().catch((e) => {
|
||||
assert.ok(e, 'Build threw');
|
||||
test('Does not complete build when Error is thrown', async () => {
|
||||
expect(() => fixture.build()).toThrow();
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Throwable.run();
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Builtins = suite('Node builtins with polyfillNode option');
|
||||
describe('Node builtins with polyfillNode option', () => {
|
||||
let fixture;
|
||||
|
||||
setup(Builtins, './fixtures/builtins-polyfillnode');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/builtins-polyfillnode/' });
|
||||
});
|
||||
|
||||
Builtins('Doesnt alias to node: prefix', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Doesn’t alias to node: prefix', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
expect($('#url').text()).toBe('unicorn.jpg');
|
||||
});
|
||||
|
||||
assert.match($('#url').text(), new RegExp('unicorn.jpg'));
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Builtins.run();
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Builtins = suite('Node builtins');
|
||||
describe('Node builtins', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Builtins, './fixtures/builtins');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/builtins/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Builtins('Can be used with the node: prefix', async ({ runtime }) => {
|
||||
// node:fs/promise is not supported in Node v12. Test currently throws.
|
||||
if (process.versions.node <= '13') {
|
||||
return;
|
||||
}
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Can be used with the node: prefix', async () => {
|
||||
// node:fs/promise is not supported in Node v12. Test currently throws.
|
||||
if (process.versions.node <= '13') {
|
||||
return;
|
||||
}
|
||||
const result = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
expect($('#version').text()).toBe('1.2.0');
|
||||
expect($('#dep-version').text()).toBe('0.0.1');
|
||||
});
|
||||
|
||||
assert.equal($('#version').text(), '1.2.0');
|
||||
assert.equal($('#dep-version').text(), '0.0.1');
|
||||
test('Throw if using the non-prefixed version', async () => {
|
||||
const result = await fixture.fetch('/bare');
|
||||
expect(result.statusCode).toBe(500);
|
||||
expect(result.body).toEqual(expect.stringContaining('Use node:fs instead'));
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Builtins('Throw if using the non-prefixed version', async ({ runtime }) => {
|
||||
const result = await runtime.load('/bare');
|
||||
assert.ok(result.error, 'Produced an error');
|
||||
assert.ok(/Use node:fs instead/.test(result.error.message));
|
||||
});
|
||||
|
||||
Builtins.run();
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { fileURLToPath } from 'url';
|
||||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { runDevServer } from './helpers.js';
|
||||
import { loadConfig } from '#astro/config';
|
||||
|
||||
const ConfigPort = suite('Config hostname');
|
||||
const MAX_TEST_TIME = 10000; // max time this test suite may take
|
||||
|
||||
const root = new URL('./fixtures/config-hostname/', import.meta.url);
|
||||
const timers = {};
|
||||
|
||||
ConfigPort.before.each(({ __test__ }) => {
|
||||
timers[__test__] = setTimeout(() => {
|
||||
throw new Error(`Test "${__test__}" did not finish within allowed time`);
|
||||
}, MAX_TEST_TIME);
|
||||
});
|
||||
|
||||
ConfigPort('can be specified in the astro config', async (context) => {
|
||||
const astroConfig = await loadConfig(fileURLToPath(root));
|
||||
assert.equal(astroConfig.devOptions.hostname, '0.0.0.0');
|
||||
});
|
||||
|
||||
ConfigPort('can be specified via --hostname flag', async (context) => {
|
||||
const args = ['--hostname', '127.0.0.1'];
|
||||
const proc = runDevServer(root, args);
|
||||
|
||||
proc.stdout.setEncoding('utf8');
|
||||
for await (const chunk of proc.stdout) {
|
||||
if (/Local:/.test(chunk)) {
|
||||
assert.ok(/:127.0.0.1/.test(chunk), 'Using the right hostname');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
ConfigPort.after.each(({ __test__ }) => {
|
||||
clearTimeout(timers[__test__]);
|
||||
});
|
||||
|
||||
ConfigPort.run();
|
|
@ -1,37 +0,0 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { runDevServer } from './helpers.js';
|
||||
|
||||
const ConfigPath = suite('Config path');
|
||||
const MAX_TEST_TIME = 10000; // max time this test suite may take
|
||||
|
||||
const root = new URL('./fixtures/config-path/', import.meta.url);
|
||||
const timers = {};
|
||||
|
||||
ConfigPath.before.each(({ __test__ }) => {
|
||||
timers[__test__] = setTimeout(() => {
|
||||
throw new Error(`Test "${__test__}" did not finish within allowed time`);
|
||||
}, MAX_TEST_TIME);
|
||||
});
|
||||
|
||||
ConfigPath('can be passed via --config', async (context) => {
|
||||
const configPath = new URL('./config/my-config.mjs', root).pathname;
|
||||
const args = ['--config', configPath];
|
||||
const process = runDevServer(root, args);
|
||||
|
||||
process.stdout.setEncoding('utf8');
|
||||
for await (const chunk of process.stdout) {
|
||||
if (/Server started/.test(chunk)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
process.kill();
|
||||
assert.ok(true, 'Server started');
|
||||
});
|
||||
|
||||
ConfigPath.after.each(({ __test__ }) => {
|
||||
clearTimeout(timers[__test__]);
|
||||
});
|
||||
|
||||
ConfigPath.run();
|
|
@ -1,43 +0,0 @@
|
|||
import { fileURLToPath } from 'url';
|
||||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { runDevServer } from './helpers.js';
|
||||
import { loadConfig } from '#astro/config';
|
||||
|
||||
const ConfigPort = suite('Config path');
|
||||
const MAX_TEST_TIME = 10000; // max time this test suite may take
|
||||
|
||||
const root = new URL('./fixtures/config-port/', import.meta.url);
|
||||
const timers = {};
|
||||
|
||||
ConfigPort.before.each(({ __test__ }) => {
|
||||
timers[__test__] = setTimeout(() => {
|
||||
throw new Error(`Test "${__test__}" did not finish within allowed time`);
|
||||
}, MAX_TEST_TIME);
|
||||
});
|
||||
|
||||
ConfigPort('can be specified in the astro config', async (context) => {
|
||||
const astroConfig = await loadConfig(fileURLToPath(root));
|
||||
assert.equal(astroConfig.devOptions.port, 3001);
|
||||
});
|
||||
|
||||
ConfigPort('can be specified via --port flag', async (context) => {
|
||||
const args = ['--port', '3002'];
|
||||
const proc = runDevServer(root, args);
|
||||
|
||||
proc.stdout.setEncoding('utf8');
|
||||
for await (const chunk of proc.stdout) {
|
||||
if (/Local:/.test(chunk)) {
|
||||
assert.ok(/:3002/.test(chunk), 'Using the right port');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
ConfigPort.after.each(({ __test__ }) => {
|
||||
clearTimeout(timers[__test__]);
|
||||
});
|
||||
|
||||
ConfigPort.run();
|
79
packages/astro/test/config.test.js
Normal file
79
packages/astro/test/config.test.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { loadConfig } from '../dist/config';
|
||||
import { devCLI, loadFixture } from './test-utils.js';
|
||||
|
||||
describe('config', () => {
|
||||
describe('hostname', () => {
|
||||
const cwd = './fixtures/config-hostname/';
|
||||
const cwdURL = new URL(cwd, import.meta.url);
|
||||
|
||||
test('can be specified in astro.config.mjs', async () => {
|
||||
const fixture = await loadFixture({
|
||||
projectRoot: cwd,
|
||||
devOptions: { hostname: '0.0.0.0' },
|
||||
});
|
||||
expect(fixture.config.devOptions.hostname).toBe('0.0.0.0');
|
||||
});
|
||||
|
||||
test('can be specified via --hostname flag', async () => {
|
||||
const args = ['--hostname', '127.0.0.1'];
|
||||
const proc = devCLI(cwdURL, args);
|
||||
|
||||
proc.stdout.setEncoding('utf8');
|
||||
for await (const chunk of proc.stdout) {
|
||||
if (/Local:/.test(chunk)) {
|
||||
expect(chunk).toEqual(expect.stringContaining('127.0.0.1'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
});
|
||||
});
|
||||
|
||||
describe('path', () => {
|
||||
const cwd = './fixtures/config-path/';
|
||||
const cwdURL = new URL(cwd, import.meta.url);
|
||||
|
||||
test('can be passed via --config', async () => {
|
||||
const configPath = new URL('./config/my-config.mjs', cwdURL).pathname;
|
||||
const args = ['--config', configPath];
|
||||
const process = devCLI(cwdURL, args);
|
||||
|
||||
process.stdout.setEncoding('utf8');
|
||||
for await (const chunk of process.stdout) {
|
||||
if (/Server started/.test(chunk)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
process.kill();
|
||||
// test will time out if the server never started
|
||||
});
|
||||
});
|
||||
|
||||
describe('port', () => {
|
||||
const cwd = './fixtures/config-port/';
|
||||
const cwdURL = new URL(cwd, import.meta.url);
|
||||
|
||||
test.skip('can be specified in astro.config.mjs', async () => {
|
||||
const config = await loadConfig(cwdURL);
|
||||
expect(config.devOptions.port).toEqual(5001);
|
||||
});
|
||||
|
||||
test.skip('can be specified via --port flag', async () => {
|
||||
const args = ['--port', '5002']; // note: this should be on the higher-end of possible ports
|
||||
const proc = devCLI(cwdURL, args);
|
||||
|
||||
proc.stdout.setEncoding('utf8');
|
||||
for await (const chunk of proc.stdout) {
|
||||
if (/Local:/.test(chunk)) {
|
||||
expect(chunk).toEqual(expect.stringContaining(':5002'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
// test will time out on a different port
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,83 +1,96 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const CustomElements = suite('Custom Elements');
|
||||
describe('Custom Elements', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(CustomElements, './fixtures/custom-elements');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({
|
||||
projectRoot: './fixtures/custom-elements/',
|
||||
renderers: ['@astrojs/test-custom-element-renderer'],
|
||||
});
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
CustomElements('Work as constructors', async ({ runtime }) => {
|
||||
const result = await runtime.load('/ctr');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Work as constructors', async () => {
|
||||
const html = await fixture.fetch('/ctr').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('my-element').length, 1, 'Element rendered');
|
||||
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
|
||||
// test 1: Element rendered
|
||||
expect($('my-element')).toHaveLength(1);
|
||||
|
||||
// test 2: shadow rendererd
|
||||
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Works with exported tagName', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Element rendered
|
||||
expect($('my-element')).toHaveLength(1);
|
||||
|
||||
// test 2: shadow rendered
|
||||
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Hydration works with exported tagName', async () => {
|
||||
const html = await fixture.fetch('/load').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// SSR
|
||||
// test 1: Element rendered
|
||||
expect($('my-element')).toHaveLength(1);
|
||||
|
||||
// test 2: shadow rendered
|
||||
expect($('my-element template[shadowroot=open]')).toHaveLength(1);
|
||||
|
||||
// Hydration
|
||||
// test 3: Component URL is included
|
||||
expect(html).toEqual(expect.stringContaining('/src/components/my-element.js'));
|
||||
});
|
||||
|
||||
test('Polyfills are added before the hydration script', async () => {
|
||||
const html = await fixture.fetch('/load').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('script[type=module]')).toHaveLength(2);
|
||||
expect($('script[type=module]').attr('src')).toBe('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
|
||||
expect($($('script[type=module]').get(1)).html()).toEqual(
|
||||
expect.stringContaining('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js')
|
||||
);
|
||||
});
|
||||
|
||||
test('Polyfills are added even if not hydrating', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('script[type=module]')).toHaveLength(1);
|
||||
expect($('script[type=module]').attr('src')).toBe('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
|
||||
expect($($('script[type=module]').get(1)).html()).not.toEqual(
|
||||
expect.stringContaining('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js')
|
||||
);
|
||||
});
|
||||
|
||||
test('Custom elements not claimed by renderer are rendered as regular HTML', async () => {
|
||||
const html = await fixture.fetch('/nossr').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Rendered the client-only element
|
||||
expect($('client-element')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Can import a client-only element that is nested in JSX', async () => {
|
||||
const html = await fixture.fetch('/nested').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// test 1: Element rendered
|
||||
expect($('client-only-element')).toHaveLength(1);
|
||||
});
|
||||
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
CustomElements('Works with exported tagName', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('my-element').length, 1, 'Element rendered');
|
||||
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
|
||||
});
|
||||
|
||||
CustomElements('Hydration works with exported tagName', async ({ runtime }) => {
|
||||
const result = await runtime.load('/load');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
// SSR
|
||||
assert.equal($('my-element').length, 1, 'Element rendered');
|
||||
assert.equal($('my-element template[shadowroot=open]').length, 1, 'shadow rendered');
|
||||
|
||||
// Hydration
|
||||
assert.ok(new RegExp('/_astro/src/components/my-element.js').test(html), 'Component URL is included');
|
||||
});
|
||||
|
||||
CustomElements('Polyfills are added before the hydration script', async ({ runtime }) => {
|
||||
const result = await runtime.load('/load');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('script[type=module]').length, 2);
|
||||
assert.equal($('script[type=module]').attr('src'), '/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
|
||||
assert.match($($('script[type=module]').get(1)).html(), new RegExp('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js'));
|
||||
});
|
||||
|
||||
CustomElements('Polyfills are added even if not hydrating', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
|
||||
assert.equal($('script[type=module]').length, 1);
|
||||
assert.equal($('script[type=module]').attr('src'), '/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/polyfill.js');
|
||||
assert.not.match($($('script[type=module]').get(1)).html(), new RegExp('/_snowpack/link/packages/astro/test/fixtures/custom-elements/my-component-lib/hydration-polyfill.js'));
|
||||
});
|
||||
|
||||
CustomElements('Custom elements not claimed by renderer are rendered as regular HTML', async ({ runtime }) => {
|
||||
const result = await runtime.load('/nossr');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('client-element').length, 1, 'Rendered the client-only element');
|
||||
});
|
||||
|
||||
CustomElements('Can import a client-only element that is nested in JSX', async ({ runtime }) => {
|
||||
const result = await runtime.load('/nested');
|
||||
assert.ok(!result.error, 'No error loading');
|
||||
const html = result.contents;
|
||||
const $ = doc(html);
|
||||
assert.equal($('client-only-element').length, 1, 'Element rendered');
|
||||
});
|
||||
|
||||
CustomElements.run();
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
const Fetch = suite('Global Fetch');
|
||||
describe('Global Fetch', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
setup(Fetch, './fixtures/fetch');
|
||||
beforeAll(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/fetch/' });
|
||||
devServer = await fixture.dev();
|
||||
});
|
||||
|
||||
Fetch('Is available in non-Astro components.', async ({ runtime }) => {
|
||||
const result = await runtime.load('/');
|
||||
assert.ok(!result.error, `build error: ${result.error}`);
|
||||
test('Is available in non-Astro components.', async () => {
|
||||
const html = await fixture.fetch('/').then((res) => res.text());
|
||||
const $ = cheerio.load(html);
|
||||
expect($('#jsx').text()).toBe('function');
|
||||
});
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#jsx').text(), 'function');
|
||||
// important: close dev server (free up port and connection)
|
||||
afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Fetch.run();
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
</style>
|
||||
<body>
|
||||
<h1>Icons</h1>
|
||||
<img src="/_astro/src/images/twitter.png" srcset="/_astro/src/images/twitter.png 1x, /_astro/src/images/twitter@2x.png 2x, /_astro/src/images/twitter@3x.png 3x" />
|
||||
<!--
|
||||
In Astro (0.20.4 and below) these srcsets will cause a build fail.
|
||||
Error (for Assets Test)
|
||||
[build] file:///astro/packages/astro/test/fixtures/astro-assets/src/pages/index.astro: could not find "/_astro/src/pages/h-300/medium_cafe_B1iTdD0C.jpg"
|
||||
-->
|
||||
<img src="../images/twitter.png" srcset="../images/twitter.png 1x, ../images/twitter@2x.png 2x, ../images/twitter@3x.png 3x" />
|
||||
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w">
|
||||
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 1.5x, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 2x">
|
||||
<!--
|
||||
|
@ -18,15 +13,7 @@
|
|||
as no checking is done for this tag (a good way to circumvent the check.)
|
||||
-->
|
||||
<picture>
|
||||
<!--
|
||||
This will cause build fail
|
||||
[build] file:///astro/packages/astro/test/fixtures/astro-assets/src/pages/index.astro: could not find "/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg"
|
||||
-->
|
||||
<!-- <source srcset="/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, /demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, /demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w"> -->
|
||||
<!--
|
||||
This will pass
|
||||
-->
|
||||
<source srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w">
|
||||
</picture>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export default {
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact',
|
||||
'@astrojs/renderer-vue',
|
||||
'@astrojs/renderer-svelte',
|
||||
],
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
renderers: [
|
||||
'@astrojs/renderer-svelte',
|
||||
],
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact'
|
||||
]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact'
|
||||
]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../",
|
||||
"devOptions": {
|
||||
"hmrPort": 5555
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
export default {
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact'
|
||||
],
|
||||
markdownOptions: {
|
||||
remarkPlugins: [
|
||||
'remark-code-titles',
|
||||
'remark-slug',
|
||||
['rehype-autolink-headings', { behavior: 'prepend' }],
|
||||
],
|
||||
rehypePlugins: [
|
||||
['rehype-toc', { headings: ["h2", "h3"] }],
|
||||
['rehype-add-classes', { 'h1,h2,h3': 'title', }],
|
||||
]
|
||||
},
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export default {
|
||||
renderers: [
|
||||
'@astrojs/renderer-preact'
|
||||
],
|
||||
buildOptions: {
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
pageUrlFormat: 'file'
|
||||
}
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev/blog/',
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev',
|
||||
},
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../",
|
||||
"packageOptions": {
|
||||
"polyfillNode": false
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
export default {
|
||||
devOptions: {
|
||||
port: 3001
|
||||
port: 5001
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
export default {
|
||||
renderers: [
|
||||
'@astrojs/test-custom-element-renderer'
|
||||
]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
export default {
|
||||
// projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
|
||||
// pages: './src/pages', // Path to Astro components, pages, and data
|
||||
// dist: './dist', // When running `astro build`, path to final static output
|
||||
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
|
||||
buildOptions: {
|
||||
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
|
||||
// sitemap: true, // Generate sitemap (set to "false" to disable)
|
||||
},
|
||||
devOptions: {
|
||||
// port: 3000, // The port to run the dev server on.
|
||||
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
||||
},
|
||||
renderers: [
|
||||
'@astrojs/renderer-lit'
|
||||
]
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"workspaceRoot": "../../../../../"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue