Skip to content

Commit 31becc9

Browse files
committed
improve logging
1 parent 3ff8a3f commit 31becc9

File tree

19 files changed

+8319
-2363
lines changed

19 files changed

+8319
-2363
lines changed

.changeset/gold-onions-kneel.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"htmldocs-starter": patch
3+
"htmldocs": patch
4+
"@htmldocs/render": patch
5+
"@htmldocs/react": patch
6+
"@htmldocs/examples": patch
7+
---
8+
9+
improve logging, dev experience

packages/htmldocs/README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
![htmldocs](https://github.com/user-attachments/assets/6f8e8ef2-022d-4418-8e86-c9663587f92f)
2+
3+
<div align="center"><strong>htmldocs</strong></div>
4+
<div align="center">Build beautiful, reactive documents with modern web technologies <br /> and generate them at scale. Batteries included.</div>
5+
<br />
6+
<div align="center">
7+
<a href="https://htmldocs.com">Website</a>
8+
<span> · </span>
9+
<a href="https://github.com/htmldocs-js/htmldocs">GitHub</a>
10+
<span> · </span>
11+
<a href="https://join.slack.com/t/htmldocs/shared_invite/zt-29hw1bnmu-ShX6Jo1KNc_XeF~gFQJH_Q">Slack</a>
12+
</div>
13+
14+
## Introduction
15+
PDF document creation is stuck in the past - from clunky Word docs to complex LaTeX to outdated tools. htmldocs brings document generation into 2025 with a modern developer experience using the tools you already love: <b>React</b>, <b>TypeScript</b>, and <b>Tailwind</b>.
16+
17+
## Why
18+
19+
htmldocs is a modern toolkit for building documents with the web:
20+
21+
- **Styling**: Use modern CSS properties to create visually stunning documents with web-like flexibility.
22+
23+
- **Structure**: Create clean layouts using HTML's powerful tools like flexbox, grid, and tables.
24+
25+
- **External Libraries**: Seamlessly integrate web libraries like FontAwesome, Bootstrap, and KaTeX
26+
27+
- **Dynamic Templates**: Leverage JSX to create reusable document templates with dynamic content:
28+
```jsx
29+
function Invoice({ customer, items, total }) {
30+
return (
31+
<Document>
32+
<Page>
33+
<h1>Invoice for {customer.name}</h1>
34+
{items.map(item => (
35+
<LineItem {...item} />
36+
))}
37+
<Total amount={total} />
38+
</Page>
39+
</Document>
40+
);
41+
}
42+
```
43+
44+
- **Data-Driven Documents**: Generate documents programmatically by passing data through props or fetching from APIs. Perfect for invoices, contracts, and reports that need dynamic content.
45+
46+
- **Version Control**: Track document changes using Git and other version control systems
47+
48+
- **Consistency**: Maintain uniform document styling across your organization through shared stylesheets.
49+
50+
## Install
51+
52+
To create your first htmldocs project, run the following command:
53+
54+
```sh
55+
npx htmldocs@latest init
56+
```
57+
58+
For further instructions or to integrate htmldocs into your existing project, refer to the [Getting Started](https://docs.htmldocs.com/getting-started) guide.
59+
60+
## Getting Started
61+
62+
Create your first document with htmldocs:
63+
64+
```jsx
65+
import { Document, Page } from "@htmldocs/react";
66+
67+
export default function MyDocument() {
68+
return (
69+
<Document size="A4" orientation="portrait" margin="0.5in">
70+
<Page style={{ backgroundColor: "#000", color: "#fff" }}>
71+
<h1>Hello from the dark side</h1>
72+
</Page>
73+
</Document>
74+
);
75+
}
76+
```
77+
78+
## Components
79+
80+
htmldocs comes with a standard set of components to help you layout and style your documents.
81+
82+
- [Document](https://docs.htmldocs.com/components/document)
83+
- [Head](https://docs.htmldocs.com/components/head)
84+
- [Page](https://docs.htmldocs.com/components/page)
85+
- [Footer](https://docs.htmldocs.com/components/footer)
86+
- [MarginBox](https://docs.htmldocs.com/components/margin-box)
87+
- [Spacer](https://docs.htmldocs.com/components/spacer)
88+
89+
## How it works
90+
91+
htmldocs is a modern toolkit for building documents with web technologies. It automatically handles the layout and chunking of your document into pages, templating variables using JSX, and hot-reloading your document.
92+
93+
htmldocs is built upon Chromium's rendering engine, which means it can render any HTML, CSS, and JavaScript. This is different from other tools like [wkhtmltopdf](https://wkhtmltopdf.org/), [WeasyPrint](https://weasyprint.org/), and [Prince](https://www.princexml.com/), which only support a subset of HTML and CSS.
94+
95+
htmldocs also uses the [Paged.js library](https://pagedjs.org/) under the hood. Paged.js is used for layout and chunking, as well as more modern features like margin boxes that aren't fully supported by the W3C's CSS standard.
96+
97+
## Tech Stack
98+
99+
| <img src="https://nextjs.org/static/favicon/favicon-32x32.png" width="48px" height="48px" alt="Next.js"> | <img src="https://www.typescriptlang.org/favicon-32x32.png" width="48px" height="48px" alt="TypeScript"> | <img src="https://user-images.githubusercontent.com/4060187/196936123-f6e1db90-784d-4174-b774-92502b718836.png" width="48px" height="48px" alt="Turborepo"> | <img src="https://pnpm.io/img/favicon.png" width="48px" height="48px" alt="pnpm"> |
100+
|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
101+
| Next.js | TypeScript | Turborepo | pnpm |
102+
103+
## Contributing
104+
105+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
106+
107+
## License
108+
109+
MIT License

packages/htmldocs/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
"tailwindcss": "^3.4.3",
7575
"tailwindcss-animate": "^1.0.7",
7676
"ts-json-schema-generator": "^2.2.0",
77-
"winston": "^3.14.2",
7877
"zod": "^3.23.8"
7978
},
8079
"devDependencies": {

packages/htmldocs/src/app/actions/render-document-by-path.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use server';
2+
23
import React from 'react';
34
import fs from 'node:fs';
45
import { getDocumentComponent } from '../../utils/get-document-component';
56
import { ErrorObject, improveErrorWithSourceMap } from '@htmldocs/render';
7+
import logger from '../lib/logger';
8+
import chalk from 'chalk';
9+
import path from 'node:path';
610

711
export interface RenderedDocumentMetadata {
812
markup: string;
@@ -26,17 +30,16 @@ export const renderDocumentByPath = async (
2630
documentPath: string,
2731
props: Record<string, any> = {}
2832
): Promise<DocumentRenderingResult> => {
29-
const isDev = process.env.NODE_ENV === 'development';
30-
isDev && console.debug(`[render] Starting render for document: ${documentPath}`);
33+
logger.debug(`[render] Starting render for document: ${documentPath}`);
3134
const startTime = performance.now();
3235

33-
isDev && console.debug('[render] Loading component...');
36+
logger.debug('[render] Loading component...');
3437
const result = await getDocumentComponent(documentPath);
3538
const componentLoadTime = performance.now() - startTime;
36-
isDev && console.debug(`[render] Component loaded in ${componentLoadTime.toFixed(2)}ms`);
39+
logger.debug(`[render] Component loaded in ${componentLoadTime.toFixed(2)}ms`);
3740

3841
if ('error' in result) {
39-
console.error('[render] Error loading component:', result.error);
42+
console.error(chalk.red('[render] Error loading component:'), result.error);
4043
return { error: result.error };
4144
}
4245

@@ -51,20 +54,25 @@ export const renderDocumentByPath = async (
5154
const DocumentComponent = Document as React.FC;
5255

5356
try {
54-
isDev && console.debug('[render] Starting rendering...');
57+
logger.debug('[render] Starting rendering...');
5558
const renderStart = performance.now();
5659
const markup = await renderAsync(<DocumentComponent {...renderProps} />, documentCss);
5760
const renderTime = performance.now() - renderStart;
58-
isDev && console.debug(`[render] Rendering completed in ${renderTime.toFixed(2)}ms`);
61+
logger.debug(`[render] Rendering completed in ${renderTime.toFixed(2)}ms`);
5962

60-
isDev && console.debug('[render] Reading file...');
63+
logger.debug('[render] Reading file...');
6164
const fileReadStart = performance.now();
6265
const reactMarkup = await fs.promises.readFile(documentPath, 'utf-8');
6366
const fileReadTime = performance.now() - fileReadStart;
64-
isDev && console.debug(`[render] File read in ${fileReadTime.toFixed(2)}ms`);
67+
logger.debug(`[render] File read in ${fileReadTime.toFixed(2)}ms`);
6568

6669
const totalTime = performance.now() - startTime;
67-
isDev && console.debug(`[render] Completed in ${totalTime.toFixed(2)}ms`);
70+
logger.debug(`[render] Completed in ${totalTime.toFixed(2)}ms`);
71+
const filename = path.basename(documentPath);
72+
const formattedTime = totalTime >= 1000
73+
? chalk.yellow(`${(totalTime / 1000).toFixed(2)}s`)
74+
: chalk.yellow(`${totalTime.toFixed(2)}ms`);
75+
console.log(`${chalk.green('✔')} Document ${chalk.cyan(filename)} rendered in ${formattedTime}`);
6876

6977
return {
7078
markup,
@@ -79,7 +87,8 @@ export const renderDocumentByPath = async (
7987
};
8088
} catch (exception) {
8189
const error = exception as Error;
82-
console.error('[render] Error during rendering:', error);
90+
const filename = path.basename(documentPath);
91+
console.error(`${chalk.red('✘')} Error during rendering ${chalk.cyan(filename)}:`, error);
8392

8493
return {
8594
error: improveErrorWithSourceMap(

packages/htmldocs/src/app/actions/render-document-to-pdf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use server"
22

33
import { LaunchOptions, chromium } from 'playwright';
4+
import logger from '~/lib/logger';
45
import { PageConfig, isStandardSize, parseCustomSize } from '~/lib/types';
56

67
export interface RenderDocumentToPDFProps extends LaunchOptions {
@@ -28,7 +29,7 @@ export const renderDocumentToPDF = async ({
2829
await page.setContent(html);
2930
await page.waitForLoadState('networkidle');
3031

31-
console.debug('pageConfig', pageConfig);
32+
logger.debug('pageConfig', pageConfig);
3233

3334
const pdfOptions = {
3435
printBackground: true,

packages/htmldocs/src/app/components/context-editor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/component
99
import { JSONSchema7 } from 'json-schema';
1010
import { useDocumentContext } from '~/contexts/document-context';
1111
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "~/components/ui/dialog";
12+
import logger from '~/lib/logger';
1213

1314
type DefaultValues = {
1415
string: string;
@@ -263,7 +264,7 @@ const ContextEditor: React.FC = () => {
263264
const { documentSchema, documentContext } = useDocumentContext();
264265

265266
const numVars = Object.keys(documentSchema?.properties || {}).length;
266-
console.debug({ documentSchema, documentContext });
267+
logger.debug({ documentSchema, documentContext });
267268

268269
return (
269270
<div className="flex flex-col gap-2 text-card-foreground">

packages/htmldocs/src/app/contexts/document-context.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { createContext, useContext, useState } from 'react';
22
import { JSONSchema7 } from 'json-schema';
3+
import logger from '~/lib/logger';
34

45
interface DocumentContextValue {
56
documentSchema: JSONSchema7;
@@ -32,7 +33,7 @@ export const DocumentContextProvider: React.FC<DocumentContextProviderProps> = (
3233
}) => {
3334
const [documentContext, setDocumentContext] = useState<Record<string, any>>({ document: initialDocumentPreviewProps || {} });
3435

35-
console.debug("Initial document context:", documentContext);
36+
logger.debug("Initial document context:", documentContext);
3637

3738
const updateDocumentContext = (path: string, newValue: any) => {
3839
const pathParts = path.split('.');

packages/htmldocs/src/app/contexts/documents.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { getDocumentPathFromSlug } from "~/actions/get-document-path-from-slug";
1313
import { renderDocumentToPDF, RenderDocumentToPDFProps } from "~/actions/render-document-to-pdf";
1414
import { PageConfig } from "~/lib/types";
15+
import logger from "~/lib/logger";
1516

1617
const DocumentsContext = createContext<
1718
| {
@@ -83,6 +84,11 @@ export const DocumentsProvider = (props: {
8384
return;
8485
}
8586

87+
// If the document is being deleted, don't try to render it
88+
if (change.event === 'unlink') {
89+
continue;
90+
}
91+
8692
const slugForChangedDocument =
8793
// filename ex: documents/apple-receipt.tsx
8894
// so we need to remove the "documents/" because it isn't used
@@ -97,7 +103,7 @@ export const DocumentsProvider = (props: {
97103
renderingResultPerDocumentPath[pathForChangedDocument];
98104

99105
if (typeof lastResult !== "undefined") {
100-
console.debug("pathForChangedDocument", pathForChangedDocument);
106+
logger.debug("pathForChangedDocument", pathForChangedDocument);
101107
const renderingResult = await renderDocumentByPath(
102108
pathForChangedDocument
103109
);

packages/htmldocs/src/app/hooks/use-hot-reload.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { useEffect, useRef } from 'react';
33
import { type Socket, io } from 'socket.io-client';
44
import type { HotReloadChange } from '../../utils/types/hot-reload-change';
55

6+
// Create a singleton socket instance
7+
let globalSocket: Socket | null = null;
8+
69
/**
710
* Hook that detects any "reload" event sent from the CLI's web socket
811
* and calls the received parameter callback
@@ -11,21 +14,19 @@ export const useHotreload = (
1114
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1215
onShouldReload: (changes: HotReloadChange[]) => any,
1316
) => {
14-
const socketRef = useRef<Socket | null>(null);
15-
1617
useEffect(() => {
17-
if (!socketRef.current) {
18-
socketRef.current = io();
18+
if (!globalSocket) {
19+
globalSocket = io();
1920
}
20-
const socket = socketRef.current;
2121

22-
socket.on('reload', (changes: HotReloadChange[]) => {
22+
globalSocket.on('reload', (changes: HotReloadChange[]) => {
2323
console.log('Reloading...');
2424
void onShouldReload(changes);
2525
});
2626

2727
return () => {
28-
socket.off();
28+
// Only remove this specific listener, don't disconnect the socket
29+
globalSocket?.off('reload');
2930
};
3031
}, [onShouldReload]);
3132
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2+
3+
const LOG_LEVELS: Record<LogLevel, number> = {
4+
debug: 0,
5+
info: 1,
6+
warn: 2,
7+
error: 3,
8+
};
9+
10+
class Logger {
11+
private static instance: Logger;
12+
private currentLevel: LogLevel;
13+
14+
private constructor() {
15+
// Default to 'info' if not set
16+
this.currentLevel = (process.env.LOG_LEVEL as LogLevel) || 'info';
17+
}
18+
19+
public static getInstance(): Logger {
20+
if (!Logger.instance) {
21+
Logger.instance = new Logger();
22+
}
23+
return Logger.instance;
24+
}
25+
26+
public setLevel(level: LogLevel): void {
27+
this.currentLevel = level;
28+
}
29+
30+
public getLevel(): LogLevel {
31+
return this.currentLevel;
32+
}
33+
34+
private shouldLog(level: LogLevel): boolean {
35+
// Only log if the message's level is >= current level
36+
return LOG_LEVELS[level] >= LOG_LEVELS[this.currentLevel];
37+
}
38+
39+
public debug(...args: any[]): void {
40+
if (!this.shouldLog('debug')) {
41+
return;
42+
}
43+
console.debug(...args);
44+
}
45+
46+
public info(...args: any[]): void {
47+
if (!this.shouldLog('info')) {
48+
return;
49+
}
50+
console.info(...args);
51+
}
52+
53+
public warn(...args: any[]): void {
54+
if (!this.shouldLog('warn')) {
55+
return;
56+
}
57+
console.warn(...args);
58+
}
59+
60+
public error(...args: any[]): void {
61+
if (!this.shouldLog('error')) {
62+
return;
63+
}
64+
console.error(...args);
65+
}
66+
}
67+
68+
// Export a singleton instance
69+
const logger = Logger.getInstance();
70+
export default logger;
71+

0 commit comments

Comments
 (0)