Fix runtime, improve code frame
This commit is contained in:
parent
7296e0b0a2
commit
6e5ede2175
9 changed files with 191 additions and 105 deletions
|
@ -73,6 +73,7 @@
|
||||||
"fast-xml-parser": "^3.19.0",
|
"fast-xml-parser": "^3.19.0",
|
||||||
"fdir": "^5.1.0",
|
"fdir": "^5.1.0",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
|
"html-entities": "^2.3.2",
|
||||||
"kleur": "^4.1.4",
|
"kleur": "^4.1.4",
|
||||||
"mime": "^2.5.2",
|
"mime": "^2.5.2",
|
||||||
"morphdom": "^2.6.1",
|
"morphdom": "^2.6.1",
|
||||||
|
@ -88,6 +89,7 @@
|
||||||
"source-map": "^0.7.3",
|
"source-map": "^0.7.3",
|
||||||
"srcset-parse": "^1.1.0",
|
"srcset-parse": "^1.1.0",
|
||||||
"string-width": "^5.0.0",
|
"string-width": "^5.0.0",
|
||||||
|
"strip-ansi": "^7.0.1",
|
||||||
"supports-esm": "^1.0.0",
|
"supports-esm": "^1.0.0",
|
||||||
"vite": "^2.5.7",
|
"vite": "^2.5.7",
|
||||||
"yargs-parser": "^20.2.9",
|
"yargs-parser": "^20.2.9",
|
||||||
|
|
|
@ -312,6 +312,8 @@ export type RSSResult = { url: string; xml?: string };
|
||||||
|
|
||||||
export type ScriptInfo = ScriptInfoInline | ScriptInfoExternal;
|
export type ScriptInfo = ScriptInfoInline | ScriptInfoExternal;
|
||||||
|
|
||||||
|
export type SSRError = Error & vite.ErrorPayload['err'];
|
||||||
|
|
||||||
export interface ScriptInfoInline {
|
export interface ScriptInfoInline {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextFunction } from 'connect';
|
import type { NextFunction } from 'connect';
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import type { AstroConfig, ManifestData, RouteCache, RouteData } from '../@types/astro';
|
import type { AstroConfig, ManifestData, RouteCache, RouteData, SSRError } from '../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
import type { HmrContext, ModuleNode } from 'vite';
|
import type { HmrContext, ModuleNode } from 'vite';
|
||||||
|
|
||||||
|
@ -11,8 +11,7 @@ import getEtag from 'etag';
|
||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
import path from 'path';
|
import stripAnsi from 'strip-ansi';
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import vite from 'vite';
|
import vite from 'vite';
|
||||||
import { defaultLogOptions, error, info } from '../logger.js';
|
import { defaultLogOptions, error, info } from '../logger.js';
|
||||||
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
|
import { createRouteManifest, matchRoute } from '../runtime/routing.js';
|
||||||
|
@ -153,6 +152,8 @@ export class AstroDevServer {
|
||||||
return this.viteServer.middlewares.handle(req, res, next);
|
return this.viteServer.middlewares.handle(req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filePath: URL | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const route = matchRoute(pathname, this.manifest);
|
const route = matchRoute(pathname, this.manifest);
|
||||||
|
|
||||||
|
@ -165,9 +166,10 @@ export class AstroDevServer {
|
||||||
this.mostRecentRoute = route;
|
this.mostRecentRoute = route;
|
||||||
|
|
||||||
// handle .astro and .md pages
|
// handle .astro and .md pages
|
||||||
|
filePath = new URL(`./${route.component}`, this.config.projectRoot);
|
||||||
const html = await ssr({
|
const html = await ssr({
|
||||||
astroConfig: this.config,
|
astroConfig: this.config,
|
||||||
filePath: new URL(`./${route.component}`, this.config.projectRoot),
|
filePath,
|
||||||
logging: this.logging,
|
logging: this.logging,
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
origin: this.origin,
|
origin: this.origin,
|
||||||
|
@ -183,12 +185,16 @@ export class AstroDevServer {
|
||||||
});
|
});
|
||||||
res.write(html);
|
res.write(html);
|
||||||
res.end();
|
res.end();
|
||||||
} catch (e) {
|
} catch (err: any) {
|
||||||
const err = e as Error;
|
|
||||||
this.viteServer.ssrFixStacktrace(err);
|
this.viteServer.ssrFixStacktrace(err);
|
||||||
console.log(err.stack);
|
this.viteServer.ws.send({ type: 'error', err });
|
||||||
const statusCode = 500;
|
const statusCode = 500;
|
||||||
const html = errorTemplate({ statusCode, title: 'Internal Error', tabTitle: '500: Error', message: err.message });
|
const html = errorTemplate({
|
||||||
|
statusCode,
|
||||||
|
title: 'Internal Error',
|
||||||
|
tabTitle: '500: Error',
|
||||||
|
message: stripAnsi(err.message),
|
||||||
|
});
|
||||||
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
|
info(this.logging, 'astro', msg.req({ url: pathname, statusCode: 500, reqTime: performance.now() - reqStart }));
|
||||||
res.writeHead(statusCode, {
|
res.writeHead(statusCode, {
|
||||||
'Content-Type': mime.getType('.html') as string,
|
'Content-Type': mime.getType('.html') as string,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { encode } from 'html-entities';
|
||||||
|
|
||||||
interface ErrorTemplateOptions {
|
interface ErrorTemplateOptions {
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
tabTitle: string;
|
tabTitle: string;
|
||||||
|
@ -29,7 +31,7 @@ export function errorTemplate({ title, message, statusCode, tabTitle }: ErrorTem
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
p {
|
pre {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -43,7 +45,7 @@ export function errorTemplate({ title, message, statusCode, tabTitle }: ErrorTem
|
||||||
<body>
|
<body>
|
||||||
<main class="wrapper">
|
<main class="wrapper">
|
||||||
<h1>${statusCode ? `<span class="statusCode">${statusCode}</span> ` : ''}${title}</h1>
|
<h1>${statusCode ? `<span class="statusCode">${statusCode}</span> ` : ''}${title}</h1>
|
||||||
<p>${message.replace(/\n/g, '<br>')}</p>
|
<pre><code>${encode(message)}</code></pre>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { AstroComponentMetadata } from '../@types/astro';
|
import type { AstroComponentMetadata } from '../@types/astro';
|
||||||
import { renderAstroComponent } from '../runtime/ssr.js';
|
|
||||||
|
|
||||||
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
import { valueToEstree, Value } from 'estree-util-value-to-estree';
|
||||||
import * as astring from 'astring';
|
import * as astring from 'astring';
|
||||||
import shorthash from 'shorthash';
|
import shorthash from 'shorthash';
|
||||||
|
import { renderAstroComponent } from '../runtime/astro.js';
|
||||||
|
|
||||||
const { generate, GENERATOR } = astring;
|
const { generate, GENERATOR } = astring;
|
||||||
// A more robust version alternative to `JSON.stringify` that can handle most values
|
// A more robust version alternative to `JSON.stringify` that can handle most values
|
||||||
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
// see https://github.com/remcohaszing/estree-util-value-to-estree#readme
|
||||||
|
|
26
packages/astro/src/runtime/astro.ts
Normal file
26
packages/astro/src/runtime/astro.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import type { AstroComponent, AstroComponentFactory } from '../internal';
|
||||||
|
|
||||||
|
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 = await componentFactory(result, props, children);
|
||||||
|
let template = await renderAstroComponent(Component);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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>');
|
||||||
|
}
|
|
@ -1,15 +1,17 @@
|
||||||
import cheerio from 'cheerio';
|
import type { BuildResult } from 'esbuild';
|
||||||
import * as eslexer from 'es-module-lexer';
|
|
||||||
import type { ViteDevServer } from 'vite';
|
import type { ViteDevServer } from 'vite';
|
||||||
import type { ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, AstroConfig } from '../@types/astro';
|
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, Params, Props, RouteCache, RouteData, RuntimeMode, SSRError } from '../@types/astro';
|
||||||
import type { LogOptions } from '../logger';
|
import type { LogOptions } from '../logger';
|
||||||
|
|
||||||
|
import cheerio from 'cheerio';
|
||||||
|
import * as eslexer from 'es-module-lexer';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { renderPage } from './astro.js';
|
||||||
import { generatePaginateFunction } from './paginate.js';
|
import { generatePaginateFunction } from './paginate.js';
|
||||||
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';
|
||||||
import { parseNpmName, canonicalURL as getCanonicalURL } from './util.js';
|
import { parseNpmName, canonicalURL as getCanonicalURL, codeFrame } from './util.js';
|
||||||
import type { AstroComponent, AstroComponentFactory } from '../internal';
|
|
||||||
|
|
||||||
interface SSROptions {
|
interface SSROptions {
|
||||||
/** an instance of the AstroConfig */
|
/** an instance of the AstroConfig */
|
||||||
|
@ -36,31 +38,6 @@ 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)
|
// 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;
|
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 = await 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();
|
const cache = new Map();
|
||||||
|
|
||||||
// TODO: improve validation and error handling here.
|
// TODO: improve validation and error handling here.
|
||||||
|
@ -155,79 +132,101 @@ async function resolveImportedModules(viteServer: ViteDevServer, file: string) {
|
||||||
|
|
||||||
/** use Vite to SSR */
|
/** use Vite to SSR */
|
||||||
export async function ssr({ astroConfig, 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
|
try {
|
||||||
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
// 1. load module
|
||||||
|
const mod = (await viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
||||||
|
|
||||||
// 1.5. resolve renderers and imported modules.
|
// 1.5. resolve renderers and imported modules.
|
||||||
// important that this happens _after_ ssrLoadModule, otherwise `importedModules` would be empty
|
// 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))]);
|
const [renderers, importedModules] = await Promise.all([resolveRenderers(viteServer, astroConfig.renderers), resolveImportedModules(viteServer, fileURLToPath(filePath))]);
|
||||||
|
|
||||||
// 2. handle dynamic routes
|
// 2. handle dynamic routes
|
||||||
let params: Params = {};
|
let params: Params = {};
|
||||||
let pageProps: Props = {};
|
let pageProps: Props = {};
|
||||||
if (route && !route.pathname) {
|
if (route && !route.pathname) {
|
||||||
if (route.params.length) {
|
if (route.params.length) {
|
||||||
const paramsMatch = route.pattern.exec(pathname)!;
|
const paramsMatch = route.pattern.exec(pathname)!;
|
||||||
params = getParams(route.params)(paramsMatch);
|
params = getParams(route.params)(paramsMatch);
|
||||||
|
}
|
||||||
|
validateGetStaticPathsModule(mod);
|
||||||
|
routeCache[route.component] =
|
||||||
|
routeCache[route.component] ||
|
||||||
|
(
|
||||||
|
await mod.getStaticPaths!({
|
||||||
|
paginate: generatePaginateFunction(route),
|
||||||
|
rss: () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).flat();
|
||||||
|
validateGetStaticPathsResult(routeCache[route.component], logging);
|
||||||
|
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
||||||
|
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
||||||
|
if (!matchedStaticPath) {
|
||||||
|
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
||||||
|
}
|
||||||
|
pageProps = { ...matchedStaticPath.props } || {};
|
||||||
}
|
}
|
||||||
validateGetStaticPathsModule(mod);
|
|
||||||
routeCache[route.component] =
|
|
||||||
routeCache[route.component] ||
|
|
||||||
(
|
|
||||||
await mod.getStaticPaths!({
|
|
||||||
paginate: generatePaginateFunction(route),
|
|
||||||
rss: () => {
|
|
||||||
/* noop */
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).flat();
|
|
||||||
validateGetStaticPathsResult(routeCache[route.component], logging);
|
|
||||||
const routePathParams: GetStaticPathsResult = routeCache[route.component];
|
|
||||||
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
|
|
||||||
if (!matchedStaticPath) {
|
|
||||||
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
|
|
||||||
}
|
|
||||||
pageProps = { ...matchedStaticPath.props } || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. render page
|
// 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
|
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);
|
const fullURL = new URL(pathname, origin);
|
||||||
|
|
||||||
const Component = await mod.default;
|
const Component = await mod.default;
|
||||||
if (!Component) throw new Error(`Expected an exported Astro component but recieved typeof ${typeof Component}`);
|
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})`);
|
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
|
||||||
|
|
||||||
let html = await renderPage(
|
let html = await renderPage(
|
||||||
{
|
{
|
||||||
styles: new Set(),
|
styles: new Set(),
|
||||||
scripts: new Set(),
|
scripts: new Set(),
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro(props: any) {
|
createAstro(props: any) {
|
||||||
const site = new URL(origin);
|
const site = new URL(origin);
|
||||||
const url = new URL('.' + pathname, site);
|
const url = new URL('.' + pathname, site);
|
||||||
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin);
|
const canonicalURL = getCanonicalURL(pathname, astroConfig.buildOptions.site || origin);
|
||||||
return { isPage: true, site, request: { url, canonicalURL }, props };
|
return { isPage: true, site, request: { url, canonicalURL }, props };
|
||||||
|
},
|
||||||
|
_metadata: { importedModules, renderers },
|
||||||
},
|
},
|
||||||
_metadata: { importedModules, renderers },
|
Component,
|
||||||
},
|
{},
|
||||||
Component,
|
null
|
||||||
{},
|
);
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. modify response
|
// 4. modify response
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
// inject Astro HMR code
|
// inject Astro HMR code
|
||||||
html = injectAstroHMR(html);
|
html = injectAstroHMR(html);
|
||||||
// inject Vite HMR code
|
// inject Vite HMR code
|
||||||
html = injectViteClient(html);
|
html = injectViteClient(html);
|
||||||
// replace client hydration scripts
|
// replace client hydration scripts
|
||||||
html = resolveNpmImports(html);
|
html = resolveNpmImports(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. finish
|
||||||
|
return html;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
|
||||||
|
if (e.errors) {
|
||||||
|
const { location, pluginName, text } = (e as BuildResult).errors[0];
|
||||||
|
const err = new Error(text) as SSRError;
|
||||||
|
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
|
||||||
|
const frame = codeFrame(await fs.promises.readFile(filePath, 'utf8'), err.loc);
|
||||||
|
err.frame = frame;
|
||||||
|
err.id = location?.file;
|
||||||
|
err.message = `${location?.file}: ${text}
|
||||||
|
|
||||||
|
${frame}
|
||||||
|
`;
|
||||||
|
err.stack = e.stack;
|
||||||
|
if (pluginName) err.plugin = pluginName;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vite error (already formatted)
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. finish
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Injects Vite client code */
|
/** Injects Vite client code */
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ErrorPayload } from 'vite';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
@ -44,3 +45,33 @@ export function parseNpmName(spec: string): { scope?: string; name: string; subp
|
||||||
subpath,
|
subpath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** generate code frame from esbuild error */
|
||||||
|
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
|
||||||
|
if (!loc) return '';
|
||||||
|
|
||||||
|
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
||||||
|
|
||||||
|
// 1. grab 2 lines before, and 3 lines after focused line
|
||||||
|
const visibleLines = [];
|
||||||
|
for (let n = -2; n <= 2; n++) {
|
||||||
|
if (lines[loc.line + n]) visibleLines.push(loc.line + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. figure out gutter width
|
||||||
|
let gutterWidth = 0;
|
||||||
|
for (const lineNo of visibleLines) {
|
||||||
|
let w = `> ${lineNo}`;
|
||||||
|
if (w.length > gutterWidth) gutterWidth = w.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. print lines
|
||||||
|
let output = '';
|
||||||
|
for (const lineNo of visibleLines) {
|
||||||
|
const isFocusedLine = lineNo === loc.line - 1;
|
||||||
|
output += isFocusedLine ? '> ' : ' ';
|
||||||
|
output += `${lineNo + 1} | ${lines[lineNo]}\n`;
|
||||||
|
if (isFocusedLine) output += `${[...new Array(gutterWidth)].join(' ')} | ${[...new Array(loc.column)].join(' ')}^\n`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -2708,6 +2708,11 @@ ansi-regex@^6.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.0.tgz#ecc7f5933cbe5ac7b33e209a5ff409ab1669c6b2"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.0.tgz#ecc7f5933cbe5ac7b33e209a5ff409ab1669c6b2"
|
||||||
integrity sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==
|
integrity sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==
|
||||||
|
|
||||||
|
ansi-regex@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
|
||||||
|
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
|
||||||
|
|
||||||
ansi-styles@^1.1.0:
|
ansi-styles@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
|
||||||
|
@ -5692,6 +5697,11 @@ html-encoding-sniffer@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-encoding "^1.0.5"
|
whatwg-encoding "^1.0.5"
|
||||||
|
|
||||||
|
html-entities@^2.3.2:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
|
||||||
|
integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==
|
||||||
|
|
||||||
html-escaper@^2.0.0:
|
html-escaper@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||||
|
@ -10720,6 +10730,13 @@ strip-ansi@^7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^6.0.0"
|
ansi-regex "^6.0.0"
|
||||||
|
|
||||||
|
strip-ansi@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||||
|
integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^6.0.1"
|
||||||
|
|
||||||
strip-bom@^3.0.0:
|
strip-bom@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||||
|
|
Loading…
Reference in a new issue