Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bun / TSC build structure & comparison #17

Merged
merged 18 commits into from
Oct 16, 2024
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
1 change: 0 additions & 1 deletion apps/ts_example/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Test Typescript compilation, using the shared TS config

type s = string;

const some_str: s = "test";
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 0 additions & 1 deletion configs/biome/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@ Create `biome.json` in your project
"extends": ["@canonical/biome-config"]
}
```

18 changes: 18 additions & 0 deletions configs/typescript-base/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,21 @@ Create a `tsconfig.json` file in the root of your project and extend the configu
"extends": "@canonical/typescript-config-base"
}
```

## Caveats

### NodeNext v Node module resolution

From gpt

When using TypeScript's NodeNext module resolution, TypeScript expects files to follow Node.js's ES Module (ESM) resolution rules, which require explicit .js extensions for imports. This approach matches ESM conventions, where the index.js file must be specified. This differs from Node module resolution, which defaults to CommonJS and allows directory imports without specifying an index file.
Best Practices:

Use Explicit File Extensions: With NodeNext, include file extensions (e.g., ./Button/index.js).
Maintain an index.ts for Exports: Collect all exports in a single index.ts at the package root for easier imports.
Consider the Module Type: Use Node resolution if CommonJS is preferred; otherwise, NodeNext for ESM compliance.

This approach maintains compatibility with both Node.js and bundlers.

In other words, by using NodeNext, we are aiming at compatibility with the ES Module specification, and this requires importing explicitly `mymodule/index.js` files instead of simply `mymodule/`

2 changes: 1 addition & 1 deletion packages/ds/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ const config: StorybookConfig = {
typescript: {
check: true,
},
staticDirs: ["../src/assets"],
staticDirs: ["../src/assets"]
};
export default config;
73 changes: 67 additions & 6 deletions packages/ds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,81 @@ This template provides a minimal setup to get React working in Vite with HMR.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md)
uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc)
uses [SWC](https://swc.rs/) for Fast Refresh

## Caveats

### TSConfig

#### Skip library check

We use [skipLibCheck](https://www.typescriptlang.org/tsconfig/#skipLibCheck) to skip type checking of declaration files.
This is needed for compatibility with vite dependencies. If this option is not enabled, the following error occurs:

We use [skipLibCheck](https://www.typescriptlang.org/tsconfig/#skipLibCheck) to
skip type checking of declaration files. This is needed for compatibility with
vite dependencies. If this option is not enabled, the following error occurs:

```
../../node_modules/@storybook/react/dist/index.d.ts(3,188): error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("storybook/internal/types")' call instead.
The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("storybook/internal/types")' call instead.
```


### Bun

[Bun](https://bun.sh/) is being experimentally used as a package manager.

### Build tool

Bun includes a [native JS bundler](https://bun.sh/docs/bundler) that can
transpile Typescript.

#### Downsides

We are not currently considering using `bun build` for production builds for
the following reasons:

##### Globstar

Bun's [implementation of globstar](https://bun.sh/docs/api/glob) is non-standard.
It is difficult to build arbitrarily deep filepaths, as Bun expects a globstar
for each level of supported nesting. Most glob implementations treat a globstar
as representing an arbitrary number of non-hidden directories. However, with
Bun, matching `.ts` files in `src/ui/Button` requires the glob pattern
`**/**/*.ts`, which does not match files in other levels of nesting.

##### Dist Depth

Generating `dist/` output that has the correct folder structure (i.e.,
`dist/ui/Button/`) is non-trivial. `bun build` generates output with folder
structure starting from matching the shallowest source file. However, this is
not always desired. For example, if `ui/` is inside `src/`, one must `cd` into
`src` before running `bun build` to generate the appropriate folder structure.
You must then set build output to `../dist` to place build results in the
project root. This makes the build script unnecessarily complex.

##### Type emitting

`bun build` does not generate types, so it must be accompanied by the usage of
some other tool that generates type declarations.

#### Upsides

##### Speed

Bun builds slightly faster than the Typescript compiler.

| Tool | Command | Real Time | User Time | Sys Time |
| ---- | --------------------------------------------------------------------| --------- | --------- | -------- |
| Bun | `bun run build:package:bun` | 0m0.648s | 0m1.498s | 0m0.117s |
| Typescript | `bun run build:package:tsc && bun run build:package:copycss` | 0m0.707s | 0m1.615s | 0m0.094s |

Note that the bun build must also call `tsc` to generate type declarations, and
the tsc build must call the external `copyfiles` dependency to copy assets into
`dist/`.

##### Non-TS bundling

`bun build` copies non-TS assets (such as images, stylesheets, etc) into `dist`.
`tsc` must be followed up with a manual step that copies non-TS files (currently,
this is only CSS) into `dist`.
21 changes: 12 additions & 9 deletions packages/ds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"scripts": {
"build": "bun run build-package",
"build": "bun run build:package",
"build:storybook": "bun run build-storybook",
"build-storybook": "storybook build",
"build-package": "tsc -p tsconfig.package.json",
"emit-declaration": "tsc --emitDeclarationOnly",
"build:package": "bun run build:package:tsc && bun run build:package:copycss",
"build:package:bun": "bun build --root src src/**/**/*.{ts,tsx} --outdir dist/esm --sourcemap=linked --external '*' && tsc -p tsconfig.build.json --emitDeclarationOnly",
"build:package:copycss": "copyfiles -u 1 src/ui/**/*.css dist/esm",
"build:package:tsc": "tsc -p tsconfig.build.json",
"lint": "biome lint src/**/*.{ts,tsx}",
"format": "bun run format:biome",
"format:biome": "biome format src/**/*.{ts,tsx}",
"check": "bun run check:biome && bun run check:ts",
"check:fix": "bun run check:biome:fix",
"check:biome": "biome check src/**/*.{ts,tsx,mdx} *.json",
"check:biome:fix": "biome check --write src/**/*.{ts,tsx,mdx} *.json",
"check:ts": "bun run check:ts:package && bun run check:ts:storybook",
"check:ts:package": "tsc -p tsconfig.package.json --noEmit",
"check:ts:storybook": "tsc --noEmit",
"check:biome": "biome check src/**/* *.json",
"check:biome:old": "biome check src/**/*.{ts,tsx,mdx} *.json",
"check:biome:fix": "biome check --write src/**/* *.json",
"check:ts": "tsc --noEmit",
"storybook": "storybook dev -p 6006 --no-open --host 0.0.0.0"
},
"dependencies": {
"react": "^19.0.0-rc-1460d67c-20241003",
"react-dom": "^19.0.0-rc-1460d67c-20241003"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.9.0",
"@canonical/biome-config": "^0.0.1",
"@canonical/typescript-config-react": "^0.0.1",
"@chromatic-com/storybook": "^1.9.0",
"@storybook/addon-essentials": "^8.3.4",
"@storybook/addon-interactions": "^8.3.4",
"@storybook/addon-links": "^8.3.4",
Expand All @@ -39,6 +41,7 @@
"@storybook/react-vite": "^8.3.4",
"@storybook/test": "^8.3.4",
"@vitejs/plugin-react": "^4.3.1",
"copyfiles": "^2.4.1",
"globals": "^15.9.0",
"storybook": "^8.3.4",
"typescript": "^5.5.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";

import { Button } from "./Button.js";
import Component from "./Button.js";

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: "Example/Button",
component: Button,
component: Component,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: "centered",
Expand All @@ -19,7 +19,7 @@ const meta = {
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ButtonProps {
}

/** Primary UI component for user interaction */
export const Button = ({
const Button = ({
primary = false,
size = "medium",
backgroundColor,
Expand All @@ -39,3 +39,5 @@ export const Button = ({
</button>
);
};

export default Button;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
border-radius: 3em;
font-weight: 700;
line-height: 1;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.storybook-button--primary {
background-color: #1ea7fd;
Expand Down
1 change: 1 addition & 0 deletions packages/ds/src/ui/Button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Button, type ButtonProps } from "./Button.js";
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";

import { Header } from "./Header.js";
import Component from "./Header.js";

const meta = {
title: "Example/Header",
component: Header,
component: Component,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ["autodocs"],
parameters: {
Expand All @@ -17,7 +17,7 @@ const meta = {
onLogout: fn(),
onCreateAccount: fn(),
},
} satisfies Meta<typeof Header>;
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type React from "react";

import { Button } from "./Button.js";
import { Button } from "../Button/index.js";
import "./header.css";

type User = {
Expand All @@ -14,7 +14,7 @@ export interface HeaderProps {
onCreateAccount?: () => void;
}

export const Header = ({
const Header = ({
user,
onLogin,
onLogout,
Expand Down Expand Up @@ -71,3 +71,5 @@ export const Header = ({
</div>
</header>
);

export default Header;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.storybook-header svg {
Expand Down
1 change: 1 addition & 0 deletions packages/ds/src/ui/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Header, type HeaderProps } from "./Header.js";
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";

import { Page } from "./Page.js";
import Component from "./Page.js";

const meta = {
title: "Example/Page",
component: Page,
component: Component,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: "fullscreen",
},
} satisfies Meta<typeof Page>;
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from "react";

import { Header } from "./Header.js";
import { Header } from "../Header/index.js";
import "./page.css";

type User = {
name: string;
};

export const Page = (): React.ReactElement => {
const Page = (): React.ReactElement => {
const [user, setUser] = React.useState<User>();

return (
Expand Down Expand Up @@ -91,3 +91,5 @@ export const Page = (): React.ReactElement => {
</article>
);
};

export default Page;
1 change: 1 addition & 0 deletions packages/ds/src/ui/Page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Page } from "./Page.js";
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
color: #333;
font-size: 14px;
line-height: 24px;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.storybook-page h2 {
Expand Down
3 changes: 3 additions & 0 deletions packages/ds/src/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Button, type ButtonProps } from "./Button/index.js";
export { Header, type HeaderProps } from "./Header/index.js";
export { Page } from "./Page/index.js";
12 changes: 12 additions & 0 deletions packages/ds/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"declaration": true,
"declarationDir": "dist/types",
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["src/**/*.stories.ts", "src/**/*.stories.tsx"]
}
8 changes: 0 additions & 8 deletions packages/ds/tsconfig.package.json

This file was deleted.