Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test

on:
push:
branches: [main, feat/*]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Run tests
run: pnpm test:ci
- name: Build
run: pnpm build
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ coverage
# nyc test coverage
.nyc_output

# Vitest
.vitest

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

Expand Down Expand Up @@ -127,4 +130,7 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*

# Git worktrees
.worktrees/
70 changes: 62 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This **[Astro integration][astro-integration]** allows developers to bundle the

## Why?

Recently, I had to build some HTML emails templates to send to some users where the final bundle is preferable to be in a single file for backend developers. That's why, only CSS and no JS files are being merged in the HTML file. If you would like to support JS files, you are welcome to submit a PR!
Recently, I had to build some HTML emails templates to send to some users where the final bundle is preferable to be in a single file for backend developers. That's why, only CSS and no JS files are being merged in the HTML file. If you would like to support JS files, you are welcome to submit a PR!

### Installation

Expand All @@ -25,9 +25,9 @@ astro add astro-single-file # Follow the instructions
```

#### Manual installation

The `astro add` command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren't sure which package manager you're using, run the first command.) Then, follow the prompts, and type "y" in the terminal (meaning "yes") for each one.

```sh
# Using NPM
npx astro add -D astro-single-file
Expand All @@ -36,28 +36,82 @@ yarn astro add -D astro-single-file
# Using PNPM
pnpx astro add -D astro-single-file
```

__`astro.config.mjs`__

```js
import astroSingleFile from 'astro-single-file';
import astroSingleFile from 'astro-single-file'

export default {
// ...
integrations: [astroSingleFile()],
}
```

Then, restart the dev server.

Then, restart the dev server.

## Usage

This integration will after the build process finishes, using `astro:build:done` hook. It will look in the build directory and it will find all the references in your HTML files where it references an external CSS file which it will replace. Then, it will remove those CSS files.

## Configuration

At the moment, this integration does not have any configuration available. You're welcome to submit an issue or PR!
### Basic Usage

```js
import astroSingleFile from 'astro-single-file'

export default {
integrations: [astroSingleFile()]
}
```

### Configuration Options

```typescript
interface SingleFileConfig {
/** Enable HTML/CSS minification (default: true) */
minify?: boolean

/** Lightning CSS transform options */
lightningcss?: {
/** Target browsers for autoprefixing */
targets?: { [key: string]: number }
/** Enable CSS drafts (nesting, etc) */
drafts?: { nesting?: boolean }
/** Additional Lightning CSS options */
[key: string]: any
}
}
```

### Examples

**Disable minification:**
```js
astroSingleFile({ minify: false })
```

**Configure browser targets:**
```js
astroSingleFile({
lightningcss: {
targets: {
safari: 13 << 16, // Safari 13+
chrome: 95 << 16 // Chrome 95+
}
}
})
```

**Enable CSS nesting:**
```js
astroSingleFile({
lightningcss: {
drafts: { nesting: true }
}
})
```

## Troubleshooting

Expand Down
138 changes: 138 additions & 0 deletions docs/plans/2025-11-01-testing-and-refactor-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Testing and Refactor Design

## Overview
Add comprehensive testing (unit + integration) and refactor the integration to be class-based with Lightning CSS for CSS transformation.

## Goals
- Add unit and integration tests using Vitest
- Refactor monolithic function into testable classes
- Add Lightning CSS for CSS transformation/minification
- Set up CI with GitHub Actions
- Adopt patterns from antfu/starter-ts

## Architecture

### Class Structure

**FileSystemAdapter Interface**
```typescript
interface FileSystemAdapter {
readDir: (path: string) => string[]
readFile: (path: string) => string
writeFile: (path: string, content: string) => void
deleteFile: (path: string) => void
stat: (path: string) => { isFile: () => boolean, isDirectory: () => boolean }
removeDir: (path: string) => void
}
```

**NodeFileSystemAdapter**
- Wraps Node's fs module
- Implements FileSystemAdapter interface
- Used in production

**MockFileSystemAdapter**
- In-memory implementation for unit tests
- Stores files in Map
- No real I/O

**CssTransformer**
- Wraps Lightning CSS
- Takes CSS string + config
- Returns transformed/minified CSS
- Handles autoprefixing, syntax lowering

**CssInliner**
- Pure function for replacing link tags with inline styles
- `replaceCss(html: string, cssFilename: string, cssContent: string): string`
- Current regex logic preserved
- No side effects

**SingleFileBuilder**
- Main orchestrator
- Receives FileSystemAdapter via constructor
- `async build(buildDir: string, config: SingleFileConfig): Promise<void>`

### File Structure
```
src/
index.ts # Entry point, creates integration
single-file-builder.ts # Main logic class
css-transformer.ts # Lightning CSS wrapper
css-inliner.ts # Pure CSS replacement logic
file-system-adapter.ts # FS interface + Node implementation
types.ts # Shared types
test/
unit/
css-inliner.test.ts
css-transformer.test.ts
single-file-builder.test.ts
integration/
astro-build.test.ts # End-to-end with real fs
```

## Configuration

**Interface:**
```typescript
interface SingleFileConfig {
minify?: boolean // Default: true
lightningcss?: import('lightningcss').TransformOptions
}
```

**Example usage:**
```typescript
astroSingleFile({
minify: true,
lightningcss: {
targets: { safari: 13 << 16 },
drafts: { nesting: true }
}
})
```

## Processing Flow
1. Find all HTML and CSS files using FileSystemAdapter
2. For each CSS file: Transform with Lightning CSS (autoprefixing, syntax lowering, minification)
3. For each HTML file: Inline transformed CSS using CssInliner
4. Minify HTML with html-minifier-terser (if minify: true)
5. Delete CSS files and empty directories

## Testing Strategy

### Unit Tests
- `css-inliner.test.ts`: Test regex replacement with various HTML structures (single CSS, multiple CSS, no CSS, malformed links)
- `css-transformer.test.ts`: Test Lightning CSS integration, verify transformations
- `single-file-builder.test.ts`: Use MockFileSystemAdapter, verify file operations, HTML/CSS processing order, minification calls

### Integration Tests
- `astro-build.test.ts`: Create real temp directories with sample HTML/CSS files, run full build, verify output
- Use actual NodeFileSystemAdapter
- Test edge cases: nested folders, empty folders cleanup, multiple HTML files

### Test Scripts
```json
"test": "vitest",
"test:unit": "vitest run test/unit",
"test:integration": "vitest run test/integration",
"test:ci": "vitest run"
```

## CI Setup
- GitHub Actions workflow `.github/workflows/test.yml`
- Run on: push, pull_request
- Node versions: 18, 20, 24
- Steps: install deps, run tests, verify build

## Dependencies to Add
- `vitest` - Test framework
- `@vitest/ui` - Test UI (optional)
- `lightningcss` - CSS transformation
- `@types/lightningcss` - TypeScript types

## Adoption from antfu/starter-ts
- Project structure patterns
- Build configuration
- Testing setup conventions
- Package.json scripts organization
Loading
Loading