Skip to content
Permalink
Browse files

Add API for frameworks and examples (#3514)

* Add API for frameworks and examples

* Adjust headers

* Update frameworks list

* Always use latest

* Add types

* Use now repo for downloading and listing

* Use .existsSync

* Remove unused packages

* Use 307 for redirect

* Add examples

* Update tsconfig.json

Co-Authored-By: Steven <steven@ceriously.com>

* Make examples unique

* Remove detectors from frameworks API

* Use /api instead of Next.js

* Install dependencies

* Rename project

* Change name

* Empty

* Change name

* Update api/tsconfig.json

Co-Authored-By: Steven <steven@ceriously.com>

* Update examples

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
AndyBitz and styfle committed Jan 7, 2020
1 parent 537d508 commit 890de6a625e1c27375db81efc21a822aba2cef6e
Showing 912 changed files with 30,138 additions and 0 deletions.
@@ -1,5 +1,6 @@
node_modules
dist
examples

# gatsby-plugin-now
packages/gatsby-plugin-now/test/fixtures
@@ -21,3 +21,5 @@ packages/now-cli/test/dev/fixtures/**/public
packages/now-cli/test/fixtures/integration
test/lib/deployment/failed-page.txt
.DS_Store
.next
public
@@ -0,0 +1,16 @@
*

# general
!.yarnrc
!run.js
!yarn.lock
!package.json

# api
!api/
!api/**

# packages
!packages/
!packages/frameworks
!packages/frameworks/**
@@ -0,0 +1,21 @@
// Currently we read & parse the README file from zeit/now-examples
// TODO: create a `manifest.json` for zeit/now-examples

import fetch from 'node-fetch';

/**
* Fetch and parse the `Frameworks and Libraries` table
* in the README file of zeit/now-examples
*/
export async function getExampleList() {
const response = await fetch(
`https://raw.githubusercontent.com/zeit/now-examples/master/manifest.json`
);

if (response.status !== 200) {
console.log('manifest.json missing in zeit/now-examples');
return null;
}

return response.json();
}
@@ -0,0 +1,18 @@
/**
* Download zip and extract to target directory
*/

import got from 'got';
import unzip from 'unzip-stream';

export async function extract(sourceUrl: string, targetPath: string) {
return new Promise((resolve, reject) => {
got
.stream(sourceUrl)
.pipe(unzip.Extract({ path: targetPath }))
.on('close', resolve)
.on('error', err => {
reject(new Error('Failed extracting from github.'));
});
});
}
@@ -0,0 +1,70 @@
import fetch from 'node-fetch';
import { Repo } from '../types';
import { getExampleList } from './example-list';

/**
* Fetch the meta info of a public github repo
* @param {object} repo parsed by the `parse-github-url` package
*/
export async function getGitHubRepoInfo(repo: Repo) {
const response = await fetch(`https://api.github.com/repos/${repo.repo}`, {
headers: {
Accept: 'application/vnd.github.machine-man-preview+json',
},
});

if (response.status !== 200) {
console.log(`Non-200 response code from GitHub: ${response.status}`);
console.log(await response.text());
return null;
}

const parsed = await response.json();

if (parsed.full_name !== repo.repo) {
console.log(`Invalid response from GitHub`);
console.log(`Received:`, parsed);
return null;
}

const data: { [key: string]: any } = {
id: parsed.full_name,
name: parsed.name,
url: parsed.html_url,
owner: parsed.owner.login,
description: parsed.description,
homepage: parsed.homepage,
size: parsed.size,
createdAt: parsed.created_at,
updatedAt: parsed.updated_at,
stars: parsed.stargazers_count,
branch: repo.branch,
};

const subdirPath = repo.repo + '/tree/' + repo.branch + '/';

if (repo.path.startsWith(subdirPath)) {
// subdir
data.subdir = repo.path.slice(subdirPath.length).split('/');
}

if (data.id === 'zeit/now-examples' && data.subdir) {
// from our examples, add `homepage` and `description` fields
const example = data.subdir[0];
const exampleList = await getExampleList();

for (const item of exampleList) {
if (item.path === `/${example}`) {
data.homepage = item.demo;
data.description = item.description;
data.exampleName = item.example;
data.icon = item.icon;
data.tagline = item.tagline;
data.framework = item.framework;
return data;
}
}
}

return data;
}
@@ -0,0 +1,45 @@
import fetch from 'node-fetch';

interface Repo {
repo: string;
owner: {
username: string;
};
username: string;
branch: string;
}

/**
* Fetch the meta info of a public gitlab repo
* @param {object} repo parsed by the `parse-github-url` package
*/
export async function getGitLabRepoInfo(repo: Repo) {
const response = await fetch(
`https://gitlab.com/api/v4/projects/${encodeURIComponent(repo.repo)}`
);

if (response.status !== 200) {
console.log(`Non-200 response code from GitLab: ${response.status}`);
return null;
}

const parsed = await response.json();
if (parsed.path_with_namespace !== repo.repo) {
console.log(`Invalid response from GitLab`);
return null;
}

return {
id: parsed.path_with_namespace,
name: parsed.path,
url: parsed.web_url,
owner: parsed.owner ? parsed.owner.username : repo.owner,
description: parsed.description,
homepage: null,
size: 0,
createdAt: parsed.created_at,
updatedAt: parsed.last_activity_at,
stars: parsed.star_count,
branch: repo.branch,
};
}
@@ -0,0 +1,27 @@
export const mapOldToNew: { [key: string]: string[] } = {
'go-image-to-ascii': ['vanilla-functions'],
markdown: ['hexo', 'docusaurus', 'docz', 'jekyll'],
'mdx-deck': ['docz'],
'mdx-deck-advanced': ['docz'],
'nextjs-mysql': ['nextjs'],
'nextjs-news': ['nextjs'],
'nextjs-nodejs-mongodb': ['nextjs'],
'nextjs-static': ['nextjs'],
'node-server': ['svelte-functions'],
nodejs: ['svelte-functions'],
'nodejs-canvas-partyparrot': ['svelte-functions'],
'nodejs-coffee': ['svelte-functions'],
'nodejs-hapi': ['svelte-functions'],
'nodejs-koa': ['svelte-functions'],
'nodejs-koa-ts': ['gatsby-functions'],
'nodejs-micro': ['svelte-functions'],
'nodejs-ms-graph-security-api': ['svelte-functions'],
'nodejs-pdfkit': ['svelte-functions'],
'nodejs-ts': ['gatsby-functions'],
'nuxt-static': ['nuxtjs'],
static: ['vanilla'],
typescript: ['gatsby-functions'],
'vanilla-go': ['vanilla-functions'],
'vanilla-json-api': ['svelte-functions'],
'vue-ssr': ['vue'],
};
@@ -0,0 +1,20 @@
/**
* Get example list from extracted folder
*/

import { join } from 'path';
import { lstatSync, existsSync, readdirSync } from 'fs';

const exists = (path: string) => existsSync(path);
const isDotFile = (name: string) => name.startsWith('.');
const isDirectory = (path: string) => lstatSync(path).isDirectory();

export function summary(source: string) {
if (!exists(source) || !isDirectory(source)) {
return [];
}

return readdirSync(source)
.filter(name => !isDotFile(name))
.filter(name => isDirectory(join(source, name)));
}
@@ -0,0 +1,9 @@
export interface Repo {
repo: string;
owner: {
username: string;
};
username: string;
branch: string;
path: string;
}
@@ -0,0 +1,29 @@
import { NowRequest, NowResponse } from '@now/node';

type Handler = (req: NowRequest, res: NowResponse) => Promise<any>;

export function withApiHandler(handler: Handler): Handler {
return async (req: NowRequest, res: NowResponse) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Accept, Content-Type'
);

if (req.method === 'OPTIONS') {
return res.status(200).json({});
}

if (req.method !== 'GET') {
return res.status(404).json({
error: {
code: 'not_found',
message: 'Only GET requests are supported for this endpoint.',
},
});
}

return handler(req, res);
};
}
@@ -0,0 +1,70 @@
import fs from 'fs';
// @ts-ignore
import tar from 'tar-fs';
import { extract } from '../../_lib/examples/extract';
import { NowRequest, NowResponse } from '@now/node';
import { withApiHandler } from '../../_lib/util/with-api-handler';

const TMP_DIR = '/tmp';

function isDirectory(path: string) {
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
}

function notFound(res: NowResponse, message: string) {
return res.status(404).send({
error: {
code: 'not_found',
message
}
});
}

function streamToBuffer(stream: any) {
return new Promise((resolve, reject) => {
const buffers: any[] = [];
stream.on('error', (err: any) => {
reject(err);
});
stream.on('data', (b: any) => {
buffers.push(b);
});
stream.on('end', () => {
resolve(Buffer.concat(buffers));
});
});
}

export default withApiHandler(async function(req: NowRequest, res: NowResponse) {
const ext = '.tar.gz';
const { segment = '' } = req.query;

if (Array.isArray(segment) || !segment.endsWith(ext)) {
return notFound(res, `Missing ${ext} suffix.`);
}

const example = segment.slice(0, -ext.length);
let directory;

if (Number(req.query.version) === 1) {
// The old cli is pinned to a specific commit hash
await extract('https://github.com/zeit/now-examples/archive/7c7b27e49b8b17d0d3f0e1604dc74fd005cd69e3.zip', TMP_DIR);
directory = `${TMP_DIR}/now-examples-7c7b27e49b8b17d0d3f0e1604dc74fd005cd69e3/${example}`;
} else {
await extract('https://github.com/zeit/now-examples/archive/master.zip', TMP_DIR);
directory = `${TMP_DIR}/now-examples-master/${example}`;

if (!isDirectory(directory)) {
// Use `now` instead of `now-examples` if the searched example does not exist
await extract('https://github.com/zeit/now/archive/master.zip', TMP_DIR);
directory = `${TMP_DIR}/now-master/examples/${example}`;
}
}

if (!isDirectory(directory)) {
return notFound(res, `Example '${example}' was not found.`);
}

const stream = tar.pack(directory);
return res.send(await streamToBuffer(stream));
});
@@ -0,0 +1,44 @@
// A proxy to get the basic info of an existing github/gitlab repo:
// GET /info?repo=zeit/micro

// @ts-ignore
import parseGitUrl from 'parse-github-url';
import { NowRequest, NowResponse } from '@now/node';
import { withApiHandler } from '../_lib/util/with-api-handler';
import { getGitHubRepoInfo } from '../_lib/examples/github-repo-info';
import { getGitLabRepoInfo } from '../_lib/examples/gitlab-repo-info';

export default withApiHandler(async function(
req: NowRequest,
res: NowResponse
) {
const repoPath = decodeURIComponent((req.query.repo as string) || '');

if (!repoPath) {
return res.status(404).json({
error: {
code: 'not_found',
message: 'Please provide the `repo` parameter.',
},
});
}

const repo = parseGitUrl(repoPath);

if (!repo.repo) {
return res.status(400).json({
error: {
code: 'invalid_repo_url',
message: 'Repository URL is invalid.',
},
});
}

if (repo.host === 'github.com') {
// URL is 'https://github.com/user/repo' or 'user/repo'
return res.json((await getGitHubRepoInfo(repo)) || {});
}

// gitlab
res.json((await getGitLabRepoInfo(repo)) || {});
});

0 comments on commit 890de6a

Please sign in to comment.
You can’t perform that action at this time.