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
5 changes: 5 additions & 0 deletions .changeset/add-ci-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
typestyles: patch
---

Add CI workflow for lint, typecheck, and test
5 changes: 5 additions & 0 deletions .changeset/add-next-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@typestyles/next': minor
---

Add @typestyles/next package for Next.js SSR integration
5 changes: 5 additions & 0 deletions .changeset/add-vite-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@typestyles/vite': patch
---

Add additional test coverage for vite plugin
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches:
- main
pull_request:

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run lint
run: pnpm run lint

typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run typecheck
run: pnpm run typecheck

test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run tests
run: pnpm run test
63 changes: 63 additions & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@typestyles/next",
"version": "0.1.0",
"description": "Next.js integration for typestyles SSR",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./src/index.d.ts",
"exports": {
".": {
"types": "./src/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsup"
},
"keywords": [
"next",
"nextjs",
"typestyles",
"ssr",
"react"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/dbanksdesign/typestyles",
"directory": "packages/next"
},
"peerDependencies": {
"next": ">=13.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"typestyles": "workspace:*"
},
"peerDependenciesMeta": {
"next": {
"optional": false
},
"react": {
"optional": false
},
"react-dom": {
"optional": false
}
},
"devDependencies": {
"@types/node": "^25.2.1",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsup": "^8.0.0",
"typescript": "^5.4.0"
}
}
13 changes: 13 additions & 0 deletions packages/next/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { getRegisteredCss } from 'typestyles/server';

export interface TypestylesStylesheetProps {
children?: React.ReactNode;
}

export function TypestylesStylesheet(props: TypestylesStylesheetProps): React.JSX.Element;

export function collectStylesFromComponent(component: React.ReactElement): Promise<string>;

export function createTypestylesLayout<P extends { children?: React.ReactNode }>(
layout: (props: P) => React.ReactNode,
): (props: P) => React.JSX.Element;
106 changes: 106 additions & 0 deletions packages/next/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useEffect, useState } from 'react';
import { collectStyles, getRegisteredCss } from 'typestyles/server';

export { getRegisteredCss };

/**
* Props for the TypestylesStylesheet component.
*/
export interface TypestylesStylesheetProps {
/**
* The children to render (styles will be collected from these).
* If not provided, only the existing registered CSS will be included.
*/
children?: React.ReactNode;
}

/**
* A React component that renders typestyles CSS.
*
* For App Router: Use this in your root layout to collect and render styles.
*
* @example
* ```tsx
* // app/layout.tsx
* import { TypestylesStylesheet } from '@typestyles/next';
*
* export default function RootLayout({ children }) {
* return (
* <html>
* <head>
* <TypestylesStylesheet />
* </head>
* <body>{children}</body>
* </html>
* );
* }
* ```
*/
export function TypestylesStylesheet({ children }: TypestylesStylesheetProps) {
const [css, setCss] = useState('');

useEffect(() => {
// On client: get any CSS that was registered before hydration
setCss(getRegisteredCss());
}, []);

if (children) {
return (
<>
{children}
<style id="typestyles" dangerouslySetInnerHTML={{ __html: css }} />
</>
);
}

return <style id="typestyles" dangerouslySetInnerHTML={{ __html: css }} />;
}

/**
* Collect styles from a React component and return them as a string.
* Useful for server components or when you need more control.
*
* @example
* ```tsx
* // app/styles.ts
* import { collectStylesFromComponent } from '@typestyles/next';
* import { YourApp } from './YourApp';
*
* export async function getStyles() {
* return collectStylesFromComponent(<YourApp />);
* }
* ```
*/
export async function collectStylesFromComponent(component: React.ReactElement): Promise<string> {
const { renderToString } = await import('react-dom/server');
const { css } = collectStyles(() => renderToString(component));
return css;
}

/**
* Create a Next.js layout export that includes typestyles.
*
* @example
* ```tsx
* // app/layout.tsx
* import { createTypestylesLayout } from '@typestyles/next';
*
* export default createTypestylesLayout(function RootLayout({ children }) {
* return (
* <html>
* <head>
* <title>My App</title>
* </head>
* <body>{children}</body>
* </html>
* );
* });
* ```
*/
export function createTypestylesLayout<P extends { children?: React.ReactNode }>(
layout: (props: P) => React.ReactNode,
): (props: P) => React.ReactNode {
return function TypestylesLayout(props: P) {
return <TypestylesStylesheet>{layout(props)}</TypestylesStylesheet>;
};
}
21 changes: 21 additions & 0 deletions packages/next/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
13 changes: 13 additions & 0 deletions packages/next/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup';

export default defineConfig({
format: ['esm', 'cjs'],
dts: false,
clean: true,
splitting: false,
entry: ['src/index.tsx'],
loader: {
'.tsx': 'tsx',
},
external: ['typestyles', 'typestyles/server', 'react', 'react-dom', 'next'],
});
32 changes: 32 additions & 0 deletions packages/vite/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ describe('extractNamespaces', () => {
expect(result.prefixes).toEqual(['.button-']);
});

it('extracts styles.component namespaces as prefixes', () => {
const code = `
import { styles } from 'typestyles';
const button = styles.component('button', {
base: { color: 'red' },
});
`;
const result = extractNamespaces(code);
expect(result.prefixes).toEqual(['.button-']);
expect(result.keys).toEqual([]);
});

it('extracts global.style selectors as prefixes', () => {
const code = `
import { global } from 'typestyles';
global.style('body', { margin: 0 });
`;
const result = extractNamespaces(code);
expect(result.prefixes).toEqual(['body']);
expect(result.keys).toEqual([]);
});

it('extracts global.fontFace as font-face prefixes', () => {
const code = `
import { global } from 'typestyles';
global.fontFace('Inter', { src: "url('/Inter.woff2')" });
`;
const result = extractNamespaces(code);
expect(result.prefixes).toEqual(['font-face:Inter']);
expect(result.keys).toEqual([]);
});

it('returns empty arrays for code with no typestyles calls', () => {
const code = `
const x = 1 + 2;
Expand Down
Loading