forked from vercel/edge-runtime
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(feature-detector): introduces edge signature detection (vercel#226)
- Loading branch information
1 parent
c184a6e
commit f34bf7a
Showing
22 changed files
with
19,101 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@edge-runtime/feature-detector': patch | ||
--- | ||
|
||
introduce hasEdgeSignature detection utility |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Callout } from 'nextra-theme-docs' | ||
import { Tabs, Tab } from '../../components/tabs' | ||
|
||
# Edge Runtime Code analysis utilities | ||
|
||
The **@edge-runtime/feature-detector** package contains utilities to analyze code running on the edge and its used low-level APIs. | ||
|
||
It leverages the excellent [ts-morph](https://ts-morph.com/) package, which is a wrapper around TypeScript compiler API to navigate its Abstract Syntaxt Tree (AST). | ||
|
||
It can analyse JavaScript and TypeScript code. | ||
|
||
<Callout type='warning' emoji='⚠️'> | ||
Please note this is an alpha version. | ||
</Callout> | ||
|
||
## Installation | ||
|
||
<Tabs items={['npm', 'yarn', 'pnpm']} storageKey='selected-pkg-manager'> | ||
<Tab>```sh npm install @edge-runtime/feature-detector --save ```</Tab> | ||
<Tab>```sh yarn add @edge-runtime/feature-detector ```</Tab> | ||
<Tab>```sh pnpm install @edge-runtime/feature-detector --save ```</Tab> | ||
</Tabs> | ||
|
||
This package includes built-in TypeScript support. | ||
|
||
## Usage | ||
|
||
Here is an example of checking whether a file's default export is a function matching the edge signature, such as: | ||
|
||
```js | ||
export default function () { | ||
return Response.json({ message: 'hello world!' }) | ||
} | ||
``` | ||
|
||
```ts | ||
import { hasEdgeSignature } from '@edge-runtime/feature-detector' | ||
|
||
const sourceFilePath = './test.js' // could be TypeScript as well. Must be in current working directory | ||
|
||
if (hasEdgeSignature(sourceFilePath)) { | ||
console.log(`${sourcefilePath} can run on the edge``) | ||
} | ||
``` | ||
|
||
## API | ||
|
||
<Callout type='warning' emoji='⚠️'> | ||
Work in progress | ||
</Callout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<div align="center"> | ||
<br> | ||
<img src="https://edge-runtime.vercel.app/og-image.png" alt="edge-runtime logo"> | ||
<br> | ||
<br> | ||
<p align="center"><strong>@edge-runtime/feature-detector</strong>: utilities to analyze code running on the edge and its used low-level APIs.</p> | ||
<p align="center">See <a href="https://edge-runtime.vercel.app/packages/feature-detector" target='_blank' rel='noopener noreferrer'>@edge-runtime/feature-detector</a> section in our <a href="https://edge-runtime.vercel.app/" target='_blank' rel='noopener noreferrer'>website</a> for more information.</p> | ||
<br> | ||
</div> | ||
|
||
## Install | ||
|
||
**Note: this is an alpha version.** | ||
|
||
Using npm: | ||
|
||
```sh | ||
npm install @edge-runtime/feature-detector --save | ||
``` | ||
|
||
or using yarn: | ||
|
||
```sh | ||
yarn add @edge-runtime/feature-detector --dev | ||
``` | ||
|
||
or using pnpm: | ||
|
||
```sh | ||
pnpm install @edge-runtime/feature-detector --save | ||
``` | ||
|
||
## License | ||
|
||
**@edge-runtime/feature-detector** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.<br> | ||
Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors). | ||
|
||
> [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import buildConfig from '../../jest.config' | ||
import type { Config } from '@jest/types' | ||
|
||
const config: Config.InitialOptions = buildConfig(__dirname) | ||
config.transform!['\\.txt$'] = 'jest-text-transformer' | ||
export default config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"name": "@edge-runtime/feature-detector", | ||
"description": "Helpers for analyzing code running on the edge and its used low-level APIs", | ||
"homepage": "https://edge-runtime.vercel.app/packages/feature-detector", | ||
"version": "1.0.0-alpha.1", | ||
"main": "dist/index.js", | ||
"module": "dist/index.mjs", | ||
"repository": { | ||
"directory": "packages/feature-detector", | ||
"type": "git", | ||
"url": "git+https://github.com/vercel/edge-runtime.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/vercel/edge-runtime/issues" | ||
}, | ||
"keywords": [], | ||
"devDependencies": { | ||
"jest-text-transformer": "1.0.4", | ||
"next": "^13.1.1", | ||
"react": "~18.2.0", | ||
"react-dom": "~18.2.0", | ||
"tsup": "^6" | ||
}, | ||
"scripts": { | ||
"build": "pnpm run build:patch && pnpm run build:src", | ||
"build:patch": "ts-node scripts/patch-type-definition.ts", | ||
"build:src": "tsup", | ||
"clean:build": "rm -rf dist", | ||
"prebuild": "pnpm run clean:build", | ||
"test": "jest" | ||
}, | ||
"license": "MPLv2", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"types": "dist/index.d.ts", | ||
"dependencies": { | ||
"ts-morph": "^17.0.1" | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
packages/feature-detector/scripts/patch-type-definition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// assumes being ran in packages/feature-detector | ||
|
||
import { get } from 'node:https' | ||
import { writeFile } from 'node:fs/promises' | ||
import { join } from 'node:path' | ||
|
||
const repositoryBaseUrl = | ||
'https://raw.githubusercontent.com/microsoft/TypeScript/main/lib' | ||
const destinationFile = join('src', 'utils/type-definition.txt') | ||
|
||
async function writeTypeDefinition() { | ||
const aggregatedContent = await fetchTypeDefinitions([ | ||
'lib.es2022.d.ts', | ||
'lib.dom.d.ts', | ||
]) | ||
await writeFile(destinationFile, patchContent(aggregatedContent).join('\n')) | ||
} | ||
|
||
async function fetchTypeDefinitions(files: string[]) { | ||
const result = [] | ||
for (const file of files) { | ||
result.push(...(await fetchTypeDefinition(file))) | ||
} | ||
return result | ||
} | ||
|
||
async function fetchTypeDefinition(file: string): Promise<string[]> { | ||
// I wish we could use node@18's fetch... | ||
const content = await new Promise<string>((resolve, reject) => | ||
get(`${repositoryBaseUrl}/${file}`, (response) => { | ||
let content = '' | ||
response.setEncoding('utf8') | ||
response.on('data', (chunk: string) => (content += chunk)) | ||
response.once('end', () => resolve(content)) | ||
response.once('error', reject) | ||
}) | ||
) | ||
const result = [] | ||
for (const line of content.split('\n')) { | ||
const reference = hasReference(line) | ||
if (reference) { | ||
result.push(...(await fetchTypeDefinition(`lib.${reference}.d.ts`))) | ||
} else { | ||
result.push(line) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
function hasReference(line: string) { | ||
const match = line.match(/^\/\/\/\s*<reference\s+lib\s*=\s*"(.+)"\s*\/>/) | ||
return match ? match[1] : null | ||
} | ||
|
||
function patchContent(lines: string[]) { | ||
const result = [] | ||
let inComment = false | ||
for (const line of lines) { | ||
if (inComment) { | ||
if (isMultiCommentEnd(line)) { | ||
inComment = false | ||
} | ||
} else { | ||
if (isMultiCommentStart(line)) { | ||
inComment = true | ||
} else if (!isSingleComment(line) && !isBlankLine(line)) { | ||
result.push(...applyPatch(line)) | ||
} | ||
} | ||
} | ||
return result | ||
} | ||
|
||
function applyPatch(line: string) { | ||
if (line === 'declare var Response: {') { | ||
// adding missing json static method | ||
// https://fetch.spec.whatwg.org/#response-class | ||
return [line, ' json(data?: any, init?: ResponseInit): Response;'] | ||
} | ||
return [line] | ||
} | ||
|
||
function isMultiCommentStart(line: string) { | ||
return /^\s*\/\*.*(?<!\*\/\s*)$/.test(line) | ||
} | ||
|
||
function isMultiCommentEnd(line: string) { | ||
return /\*\/\s*$/.test(line) | ||
} | ||
|
||
function isSingleComment(line: string) { | ||
return /^(?!\s*\/\/\/ <r)\s*\/\/|^\s*\/\*.+\*\//.test(line) | ||
} | ||
|
||
function isBlankLine(line: string) { | ||
return /^\s*$/.test(line) | ||
} | ||
|
||
writeTypeDefinition() | ||
.then(() => process.exit(0)) | ||
.catch((error) => { | ||
console.log('Errored', error) | ||
process.exit(1) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Project } from 'ts-morph' | ||
import { getDefaultExport } from './utils/exports' | ||
import { getReturnType } from './utils/functions' | ||
import { buildProject } from './utils/project' | ||
import { extractFromPromise, isSubClassOf } from './utils/types' | ||
|
||
/** | ||
* Returns true if the default export of the provided file is a function returning a web Response object. | ||
*/ | ||
export function hasEdgeSignature( | ||
sourcePath: string, | ||
project: Project = buildProject() | ||
) { | ||
const sourceFile = project.addSourceFileAtPath(sourcePath) | ||
const defaultExport = getDefaultExport(sourceFile) | ||
if (!defaultExport) { | ||
return false | ||
} | ||
const returnType = getReturnType(defaultExport) | ||
if (!returnType) { | ||
return false | ||
} | ||
return isSubClassOf(extractFromPromise(returnType), 'Response') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './has-edge-signature' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Node, SourceFile } from 'ts-morph' | ||
|
||
/** | ||
* Get the default export of a modules, if any. | ||
* Supported: | ||
* - export definition: `export default function foo() {}` | ||
* - exported declaration: `const foo = 'bar'; export default foo` | ||
*/ | ||
export function getDefaultExport(sourceFile: SourceFile): Node | undefined { | ||
const defaultExport = sourceFile.getDefaultExportSymbol() | ||
return ( | ||
defaultExport?.getValueDeclaration() ?? | ||
defaultExport?.getDeclarations()?.[0] // when exporting a variable: `export default handler` | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Node, SyntaxKind, Type } from 'ts-morph' | ||
|
||
const { ExportAssignment, Identifier, FunctionDeclaration, ArrowFunction } = | ||
SyntaxKind | ||
|
||
/** | ||
* Exteracts the return type of a function, if any. | ||
* Supports: | ||
* - function declaration `function foo() {}` | ||
* - function expression `const foo = function() {}` | ||
* - arrow functions `const foo = () => {}` | ||
* - module exports | ||
*/ | ||
export function getReturnType(node?: Node): Type | undefined { | ||
switch (node?.getKind()) { | ||
case ExportAssignment: | ||
return getReturnType(node.asKind(ExportAssignment)?.getExpression()) | ||
case Identifier: | ||
return getReturnType(node.getSymbol()?.getValueDeclaration()) | ||
case FunctionDeclaration: | ||
return node.asKind(FunctionDeclaration)?.getReturnType() | ||
case ArrowFunction: | ||
return node.asKind(ArrowFunction)?.getReturnType() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Project } from 'ts-morph' | ||
// @ts-ignore will be resolved by tsup & jest | ||
import typeDefinitionContent from './type-definition.txt' | ||
|
||
/** | ||
* Builds a TS-morph project that allows JS code and loads our custom Response + Promise types | ||
*/ | ||
export function buildProject() { | ||
const project = new Project({ | ||
compilerOptions: { | ||
allowJs: true, | ||
}, | ||
}) | ||
project.createSourceFile('node_modules/index.d.ts', typeDefinitionContent) | ||
project.addDirectoryAtPathIfExists('.') | ||
return project | ||
} |
Oops, something went wrong.