Skip to content
This repository has been archived by the owner on Feb 7, 2023. It is now read-only.

Auto-Generated Types #112

Merged
merged 11 commits into from
Sep 30, 2021
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '12.x'
node-version: '16'
- run: npm install -g markdownlint-cli@0.23.2
- run: markdownlint '**/*.md' --ignore node_modules
18 changes: 18 additions & 0 deletions .github/workflows/overrides.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Check Docs & Overrides

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '16'
- run: npm install
- name: Check Docs
run: npm run export:docs
- name: Check Overrides
run: npm run export:overrides
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.DS_Store
.vscode
node_modules
docs.json
overrides.json
7 changes: 7 additions & 0 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ MD013: false
# MD014/commands-show-output Dollar signs used before commands without showing output
MD014: false

# MD033/no-inline-html Inline HTML (used in README.md)
MD033:
allowed_elements:
- sup
- sub
- code

# heading duplication is allowed for non-sibling headings (common in change logs)
MD024:
siblings_only: true
4 changes: 2 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"singleQuote": true,
"printWidth": 100
"printWidth": 120,
"proseWrap": "always"
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 3.0.0

### Features

- **Types are automatically generated from the runtime - [@mrbbot], [pull/112]**
Types now match exactly what's defined in the runtime source code, meaning `webworker` should be removed from users' `tsconfig.json`s

[@mrbbot]: https://github.com/mrbbot
[pull/112]: https://github.com/cloudflare/workers-types/pull/112

## 2.2.2

### Features
Expand Down
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Cloudflare Workers Types

> :warning: If you're upgrading from version 2, make sure to remove `webworker` from the `lib` array in your
> `tsconfig.json`. These types are now included in `@cloudflare/workers-types`.

## Install

```bash
npm install @cloudflare/workers-types
npm install -D @cloudflare/workers-types
-- Or
yarn add @cloudflare/workers-types
yarn add -D @cloudflare/workers-types
```

## Usage
Expand All @@ -19,24 +22,55 @@ The following is a minimal `tsconfig.json` for use alongside this package:
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020", "WebWorker"],
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"]
}
}
```

### Using bindings

It's recommended that you create an ambient type file for any bindings your Worker uses. Create a file named `bindings.d.ts` in your src directory:
It's recommended that you create an ambient type file for any bindings your Worker uses. Create a file named
`bindings.d.ts` in your src directory:

**`bindings.d.ts`**

```typescript
export {};

declare global {
const MY_ENV_VAR: string
const MY_SECRET: string
const myKVNamespace: KVNamespace
const MY_ENV_VAR: string;
const MY_SECRET: string;
const myKVNamespace: KVNamespace;
}
```

## Auto-Generation

Types are automatically generated from the Workers runtime's source code on every release. However, these generated
types don't include any generics or overloads, so to improve ergonomics, some of them are overridden.

The [`overrides`](./overrides) directory contains partial TypeScript declarations. If an override has a different type
classification than its generated counterpart – for example, an `interface` is declared to override a generated `class`
definition – then the override is used instead of the generated output. However, if they're the same type classification
(e.g. both the override and the generated output are using `class`), then their member properties are merged:

- Members in the override but not in the generated type are included in the output
- If a member in the override has the same name as one in the generated type, the generated one is removed from the
output, and the override is included instead
- If the member is declared type `never` in the override, it is removed from the output

If a named type override is declared as a type alias to `never`, that named type is removed from the output.

JSDoc comments from overrides will also be copied over to the output.

Comment overrides can also be written in Markdown. The [`docs`](./docs) directory contains these overrides.
2<sup>nd</sup> level headings are the names of top level declarations (e.g. <code>## \`KVNamespace\`</code>),
3<sup>rd</sup> level headings are for member names (e.g. <code>### \`KVNamespace#put\`</code>), and 4<sup>th</sup> level
headings correspond to JSDoc sections for members:

- `#### Parameters`: a list with parameters of the form <code>- \`param\`: param description</code>, these will be
formatted as `@param` tags
- `#### Returns`: contents will be copied as-is to the `@returns` tag
- `#### Examples`: fenced code blocks with the language set to `js`, `ts`, `javascript` or `typescript` will be copied
to `@example` tags
25 changes: 25 additions & 0 deletions docs/kv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# KV

## `KVNamespace`

Workers KV is a global, low-latency, key-value data store. It supports exceptionally high read volumes with low-latency,
making it possible to build highly dynamic APIs and websites which respond as quickly as a cached static file would.

### `KVNamespace#put`

Creates a new key-value pair, or updates the value for a particular key.

#### Parameters

- `key`: key to associate with the value. A key cannot be empty, `.` or `..`. All other keys are valid.
- `value`: value to store. The type is inferred. The maximum size of a value is 25MB.

#### Returns

Returns a `Promise` that you should `await` on in order to verify a successful update.

#### Examples

```js
await NAMESPACE.put(key, value)
```
161 changes: 161 additions & 0 deletions export/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/// <reference types="node" />
import * as fs from "fs";
import * as path from "path";
// marked is great for this: it includes the raw text in its tokens so
// we don't need to write code that renders tokens back to markdown
import marked from "marked";
import { Comment, CommentParam } from "./types";

interface CommentedField {
name: string;
comment?: Comment;
}

interface CommentedDeclaration {
name: string;
comment?: Comment;
members?: CommentedField[];
}

// Get files to build docs from
const docsDir = path.join(__dirname, "..", "docs");
const filePaths = fs.readdirSync(docsDir).map((fileName) => path.join(docsDir, fileName));

// Maps fenced code-block languages to those recognised by declaration renderers
const exampleLangRenames = {
js: "typescript",
ts: "typescript",
javascript: "typescript",
rs: "rust",
};

function trimComment(comment?: Comment) {
if (comment === undefined) return;
comment.text = comment.text.trim();
if (comment.params) {
comment.params = comment.params.map(({ name, text }) => ({ name, text: text.trim() }));
}
if (comment.returns) {
comment.returns = comment.returns.trim();
}
}

const declarations: Record<string, CommentedDeclaration> = {};
let declaration: CommentedDeclaration | undefined = undefined;
let field: CommentedField | undefined = undefined;

enum FieldState {
// Enum member names must case-sensitive match expected 4th level heading texts
Parameters,
Returns,
Examples,
}
let fieldState: FieldState | undefined = undefined;

function pushDeclaration() {
/// Adds the current declaration (if any) to the map
if (declaration !== undefined) {
trimComment(declaration.comment);
declarations[declaration.name] = declaration;

declaration = undefined;
fieldState = undefined;
}
}

function pushField() {
/// Adds the current field (if any) to the current declaration
if (declaration !== undefined && field !== undefined) {
trimComment(field.comment);
declaration.members ??= [];
declaration.members.push(field);

field = undefined;
fieldState = undefined;
}
}

for (const filePath of filePaths) {
const tokens = marked.lexer(fs.readFileSync(filePath, "utf8"));

for (const token of tokens) {
if (token.type === "heading" && token.depth === 2) {
// New declaration
pushDeclaration();
// token.text === "`Declaration`"
declaration = { name: token.text.substring(1, token.text.length - 1) };
continue;
} else if (declaration === undefined) {
// If this isn't a new declaration, wait until we've got a declaration to add to
continue;
}

if (token.type === "heading" && token.depth === 3) {
// New field
pushField();
// token.text === "`Declaration.field`" or "`Declaration#field`"
field = { name: token.text.substring(1 + declaration.name.length + 1, token.text.length - 1) };
continue;
}

if (field && token.type === "heading" && token.depth === 4) {
fieldState = undefined;
if (token.text in FieldState) {
fieldState = FieldState[token.text];
continue;
}
}

if (field && fieldState === FieldState.Parameters && token.type === "list") {
// Field parameters
field.comment ??= { text: "" };
field.comment.params ??= [];
field.comment.params.push(
...token.items.map<CommentParam>((item) => {
// item.text === "`name`: text" (text will be trimmed later by trimComment)
const colon = item.text.indexOf(":");
return {
name: item.text.substring(1, colon - 1),
text: item.text.substring(colon + 1),
};
})
);
continue;
}

if (field && fieldState === FieldState.Returns) {
// Field returns
field.comment ??= { text: "" };
field.comment.returns ??= "";
field.comment.returns += token.raw;
continue;
}

if (field && fieldState === FieldState.Examples && token.type === "code") {
// Field examples
if (!token.lang) continue;
let lang = token.lang;
if (lang in exampleLangRenames) lang = exampleLangRenames[lang];
field.comment ??= { text: "" };
field.comment.examples ??= {};
field.comment.examples[lang] ??= [];
field.comment.examples[lang].push(token.text);
continue;
}

// If we're in a field, add comments to that, otherwise add them to the declaration itself
if (field) {
field.comment ??= { text: "" };
field.comment.text += token.raw;
} else if (declaration) {
declaration.comment ??= { text: "" };
declaration.comment.text += token.raw;
}
}

// Record final field and declaration (if any)
pushField();
pushDeclaration();
}

fs.writeFileSync("docs.json", JSON.stringify(declarations, null, 2), "utf8");
Loading