forked from vercel/vercel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
@vercel/static-config
(vercel#6897)
- Loading branch information
1 parent
b890ac1
commit 9c67e81
Showing
13 changed files
with
339 additions
and
1 deletion.
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
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 @@ | ||
test/fixtures |
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 @@ | ||
/dist |
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,42 @@ | ||
{ | ||
"name": "@vercel/static-config", | ||
"version": "0.0.0", | ||
"license": "MIT", | ||
"main": "./dist/index", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/vercel/vercel.git", | ||
"directory": "packages/static-config" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"test-unit": "jest", | ||
"prepublishOnly": "tsc" | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"dependencies": { | ||
"ajv": "8.6.3", | ||
"json-schema-to-ts": "1.6.4", | ||
"ts-morph": "12.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "27.0.2", | ||
"@types/node": "*" | ||
}, | ||
"jest": { | ||
"preset": "ts-jest", | ||
"globals": { | ||
"ts-jest": { | ||
"diagnostics": false, | ||
"isolatedModules": true | ||
} | ||
}, | ||
"verbose": false, | ||
"testEnvironment": "node", | ||
"testMatch": [ | ||
"<rootDir>/test/**/*.test.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,120 @@ | ||
import { | ||
Project, | ||
SourceFile, | ||
SyntaxKind, | ||
NodeFlags, | ||
ObjectLiteralExpression, | ||
Node, | ||
ArrayLiteralExpression, | ||
} from 'ts-morph'; | ||
import Ajv from 'ajv'; | ||
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; | ||
|
||
const ajv = new Ajv(); | ||
|
||
export const BaseFunctionConfigSchema = { | ||
type: 'object', | ||
properties: { | ||
runtime: { type: 'string' }, | ||
memory: { type: 'number' }, | ||
maxDuration: { type: 'number' }, | ||
regions: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
}, | ||
}, | ||
} as const; | ||
|
||
export type BaseFunctionConfig = FromSchema<typeof BaseFunctionConfigSchema>; | ||
|
||
export function getConfig< | ||
T extends JSONSchema = typeof BaseFunctionConfigSchema | ||
>(project: Project, sourcePath: string, schema?: T): FromSchema<T> | null { | ||
const sourceFile = project.addSourceFileAtPath(sourcePath); | ||
const configNode = getConfigNode(sourceFile); | ||
if (!configNode) return null; | ||
const config = getValue(configNode); | ||
// @ts-ignore | ||
return validate(schema || BaseFunctionConfigSchema, config); | ||
} | ||
|
||
function validate<T>(schema: T, data: any): FromSchema<T> { | ||
const isValid = ajv.compile(schema); | ||
if (!isValid(data)) { | ||
// TODO: better error message | ||
throw new Error('Invalid data'); | ||
} | ||
return data as FromSchema<T>; | ||
} | ||
|
||
function getConfigNode(sourceFile: SourceFile) { | ||
return sourceFile | ||
.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression) | ||
.find(objectLiteral => { | ||
// Make sure the object is assigned to "config" | ||
const varDec = objectLiteral.getParentIfKind( | ||
SyntaxKind.VariableDeclaration | ||
); | ||
if (varDec?.getName() !== 'config') return false; | ||
|
||
// Make sure assigned with `const` | ||
const varDecList = varDec.getParentIfKind( | ||
SyntaxKind.VariableDeclarationList | ||
); | ||
const isConst = (varDecList?.getFlags() ?? 0) & NodeFlags.Const; | ||
if (!isConst) return false; | ||
|
||
// Make sure it is exported | ||
const exp = varDecList?.getParentIfKind(SyntaxKind.VariableStatement); | ||
if (!exp?.isExported()) return false; | ||
|
||
return true; | ||
}); | ||
} | ||
|
||
function getValue(valueNode: Node): unknown { | ||
if (Node.isStringLiteral(valueNode)) { | ||
return eval(valueNode.getText()); | ||
} else if (Node.isNumericLiteral(valueNode)) { | ||
return Number(valueNode.getText()); | ||
} else if (Node.isTrueLiteral(valueNode)) { | ||
return true; | ||
} else if (Node.isFalseLiteral(valueNode)) { | ||
return false; | ||
} else if (Node.isNullLiteral(valueNode)) { | ||
return null; | ||
} else if (Node.isArrayLiteralExpression(valueNode)) { | ||
return getArray(valueNode); | ||
} else if (Node.isObjectLiteralExpression(valueNode)) { | ||
return getObject(valueNode); | ||
} else if ( | ||
Node.isIdentifier(valueNode) && | ||
valueNode.getText() === 'undefined' | ||
) { | ||
return undefined; | ||
} | ||
throw new Error( | ||
`Unhandled type: "${valueNode.getKindName()}" ${valueNode.getText()}` | ||
); | ||
} | ||
|
||
function getObject(obj: ObjectLiteralExpression): unknown { | ||
const rtn: { [v: string]: unknown } = {}; | ||
for (const prop of obj.getProperties()) { | ||
if (!Node.isPropertyAssignment(prop)) continue; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const [nameNode, _colon, valueNode] = prop.getChildren(); | ||
const name = nameNode.getText(); | ||
rtn[name] = getValue(valueNode); | ||
} | ||
return rtn; | ||
} | ||
|
||
function getArray(arr: ArrayLiteralExpression): unknown { | ||
const elementNodes = arr.getElements(); | ||
const rtn = new Array(elementNodes.length); | ||
for (let i = 0; i < elementNodes.length; i++) { | ||
rtn[i] = getValue(elementNodes[i]); | ||
} | ||
return rtn; | ||
} |
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,11 @@ | ||
import ms from 'https://denopkg.com/TooTallNate/ms'; | ||
import { readerFromStreamReader } from 'https://deno.land/std@0.107.0/io/streams.ts'; | ||
|
||
export const config = { | ||
runtime: 'deno', | ||
location: 'https://example.com/page', | ||
}; | ||
|
||
export default async ({ request }: Deno.RequestEvent) => { | ||
return new Response('Hello from Deno'); | ||
}; |
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,3 @@ | ||
export const config = { | ||
runtime: 0, | ||
}; |
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 const foo = 'bar'; |
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,10 @@ | ||
import fs from 'fs'; | ||
|
||
export const config = { | ||
runtime: 'node', | ||
memory: 1024, | ||
}; | ||
|
||
export default function (req, res) { | ||
res.end('Hi from Node'); | ||
} |
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,53 @@ | ||
import { join } from 'path'; | ||
import { Project } from 'ts-morph'; | ||
import { getConfig } from '../src'; | ||
|
||
describe('getConfig()', () => { | ||
it('should parse config from Node.js file', () => { | ||
const project = new Project(); | ||
const sourcePath = join(__dirname, 'fixtures/node.js'); | ||
const config = getConfig(project, sourcePath); | ||
expect(config).toMatchInlineSnapshot(` | ||
Object { | ||
"memory": 1024, | ||
"runtime": "node", | ||
} | ||
`); | ||
}); | ||
|
||
it('should parse config from Deno file', () => { | ||
const project = new Project(); | ||
const sourcePath = join(__dirname, 'fixtures/deno.ts'); | ||
const config = getConfig(project, sourcePath, { | ||
type: 'object', | ||
properties: { | ||
location: { type: 'string' }, | ||
}, | ||
} as const); | ||
expect(config).toMatchInlineSnapshot(` | ||
Object { | ||
"location": "https://example.com/page", | ||
"runtime": "deno", | ||
} | ||
`); | ||
}); | ||
|
||
it('should return `null` when no config was exported', () => { | ||
const project = new Project(); | ||
const sourcePath = join(__dirname, 'fixtures/no-config.js'); | ||
const config = getConfig(project, sourcePath); | ||
expect(config).toBeNull(); | ||
}); | ||
|
||
it('should throw an error upon schema validation failure', () => { | ||
const project = new Project(); | ||
const sourcePath = join(__dirname, 'fixtures/invalid-schema.js'); | ||
let err; | ||
try { | ||
getConfig(project, sourcePath); | ||
} catch (_err) { | ||
err = _err; | ||
} | ||
expect(err.message).toEqual('Invalid data'); | ||
}); | ||
}); |
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,4 @@ | ||
{ | ||
"extends": "../tsconfig.json", | ||
"include": ["*.test.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,16 @@ | ||
{ | ||
"compilerOptions": { | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"lib": ["esnext"], | ||
"target": "es2018", | ||
"module": "commonjs", | ||
"outDir": "dist", | ||
"sourceMap": true, | ||
"declaration": true, | ||
"moduleResolution": "node", | ||
"typeRoots": ["./@types", "./node_modules/@types"] | ||
}, | ||
"include": ["src/**/*"], | ||
"exclude": ["node_modules"] | ||
} |
Oops, something went wrong.