Skip to content

Commit

Permalink
initial commit of vercel plugin fork to support the new functions arg…
Browse files Browse the repository at this point in the history
…ument
  • Loading branch information
hydrosquall committed Apr 16, 2022
1 parent 2ffc230 commit 1fb4646
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 1 deletion.
1 change: 0 additions & 1 deletion demo/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pysqlite3-binary
sqlite-utils>=3.2
datasette>=0.61
datasette-publish-vercel>=0.11
319 changes: 319 additions & 0 deletions plugins/datasette_publish_vercel_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
# This is a fork of
# https://github.com/simonw/datasette-publish-vercel/blob/main/datasette_publish_vercel/__init__.py
# Goal: support the functions argument

from datasette import hookimpl
from datasette.publish.common import (
add_common_publish_arguments_and_options,
fail_if_publish_binary_not_installed,
)
from datasette.utils import (
temporary_docker_directory,
value_as_boolean,
ValueAsBooleanError,
)
from subprocess import run
import click
from click.types import CompositeParamType
import json
import os
import pathlib
import re
import shutil

INDEX_PY = """
import asyncio
from datasette.app import Datasette
import json
import pathlib
import os
static_mounts = [
(static, str((pathlib.Path(".") / static).resolve()))
for static in {statics}
]
metadata = dict()
try:
metadata = json.load(open("metadata.json"))
except Exception:
pass
secret = os.environ.get("DATASETTE_SECRET")
ds = Datasette(
[],
{database_files},
static_mounts=static_mounts,
metadata=metadata{extras},
secret=secret,
cors=True,
settings={settings}{crossdb}
)
asyncio.run(ds.invoke_startup())
app = ds.app()
""".strip()

project_name_re = re.compile(r"^[a-z0-9][a-z0-9-]{1,51}$")


class Setting(CompositeParamType):
name = "setting"
arity = 2

def convert(self, config, param, ctx):
from datasette.app import DEFAULT_SETTINGS

name, value = config
if name not in DEFAULT_SETTINGS:
self.fail(
f"{name} is not a valid option (--help-config to see all)",
param,
ctx,
)
return
# Type checking
default = DEFAULT_SETTINGS[name]
if isinstance(default, bool):
try:
return name, value_as_boolean(value)
except ValueAsBooleanError:
self.fail(f'"{name}" should be on/off/true/false/1/0', param, ctx)
return
elif isinstance(default, int):
if not value.isdigit():
self.fail(f'"{name}" should be an integer', param, ctx)
return
return name, int(value)
elif isinstance(default, str):
return name, value
else:
# Should never happen:
self.fail("Invalid option")


class ProjectName(click.ParamType):
name = "project"

def convert(self, value, param, ctx):
if not project_name_re.match(value):
self.fail(
"Project name must be lowercase, alphanumeric, max 52 chars, cannot begin with a hyphen"
)
return value


def add_vercel_options(cmd):
for decorator in reversed(
(
click.option("--token", help="Auth token to use for deploy"),
click.option(
"--project",
type=ProjectName(),
help="Vercel project name to use",
required=True,
),
click.option(
"--scope",
help="Optional Vercel scope (e.g. a team name)",
),
click.option(
"--no-prod",
is_flag=True,
help="Don't deploy directly to production",
),
click.option(
"--debug",
is_flag=True,
help="Enable Vercel CLI debug output",
),
click.option(
"--public",
is_flag=True,
help="Publish source with Vercel CLI --public",
),
click.option(
"--generate-dir",
type=click.Path(dir_okay=True, file_okay=False),
help="Output generated application files and stop without deploying",
),
click.option(
"--generate-vercel-json",
is_flag=True,
help="Output generated vercel.json file and stop without deploying",
),
click.option(
"--vercel-json",
type=click.File(),
help="Custom vercel.json file to use instead of generating one",
),
click.option(
"--setting",
"settings",
type=Setting(),
help="Setting, see docs.datasette.io/en/stable/settings.html",
multiple=True,
),
click.option(
"--crossdb", is_flag=True, help="Enable cross-database SQL queries"
),
)
):
cmd = decorator(cmd)
return cmd


def _publish_vercel(
files,
metadata,
extra_options,
branch,
template_dir,
plugins_dir,
static,
install,
plugin_secret,
version_note,
secret,
title,
license,
license_url,
source,
source_url,
about,
about_url,
token,
project,
scope,
no_prod,
debug,
public,
generate_dir,
generate_vercel_json,
vercel_json,
settings,
crossdb,
):
if vercel_json and generate_vercel_json:
raise click.ClickException(
"Cannot use both --vercel-json and --generate-vercel-json"
)
fail_if_publish_binary_not_installed(
"vercel", "Vercel", "https://vercel.com/download"
)
extra_metadata = {
"title": title,
"license": license,
"license_url": license_url,
"source": source,
"source_url": source_url,
"about": about,
"about_url": about_url,
}

if generate_dir:
generate_dir = str(pathlib.Path(generate_dir).resolve())

vercel_json_content = json.dumps(
{
"name": project,
"version": 2,
"builds": [{"src": "index.py", "use": "@vercel/python"}],
"routes": [{"src": "(.*)", "dest": "index.py"}],
},
indent=4,
)
if generate_vercel_json:
click.echo(vercel_json_content)
return

if vercel_json:
vercel_json_content = vercel_json.read()
try:
json.loads(vercel_json_content)
except ValueError:
raise click.ClickException("--vercel-json contents must be valid JSON")

with temporary_docker_directory(
files,
"datasette-now-v2",
metadata,
extra_options,
branch,
template_dir,
plugins_dir,
static,
install,
False,
version_note,
secret,
extra_metadata,
port=8080,
):
# We don't actually want the Dockerfile
os.remove("Dockerfile")
open("vercel.json", "w").write(vercel_json_content)
extras = []
if template_dir:
extras.append('template_dir="templates"')
if plugins_dir:
extras.append('plugins_dir="plugins"')

statics = [item[0] for item in static]

open("index.py", "w").write(
INDEX_PY.format(
database_files=json.dumps([os.path.split(f)[-1] for f in files]),
extras=", {}".format(", ".join(extras)) if extras else "",
statics=json.dumps(statics),
settings=json.dumps(dict(settings) or {}),
crossdb=",\n crossdb=True" if crossdb else "",
)
)
datasette_install = "datasette"
if branch:
datasette_install = (
"https://github.com/simonw/datasette/archive/{}.zip".format(branch)
)
open("requirements.txt", "w").write(
"\n".join([datasette_install, "pysqlite3-binary"] + list(install))
)
if generate_dir:
# Copy these to the specified directory
shutil.copytree(".", generate_dir)
click.echo(
"Your generated application files have been written to:", err=True
)
click.echo(" {}\n".format(generate_dir), err=True)
click.echo("To deploy using Vercel, run the following:")
click.echo(" cd {}".format(generate_dir), err=True)
click.echo(" vercel --prod".format(generate_dir), err=True)
else:
# Run the deploy with Vercel
cmd = ["vercel", "--confirm", "--no-clipboard"]
if debug:
cmd.append("--debug")
if not no_prod:
cmd.append("--prod")
if public:
cmd.append("--public")
if token:
cmd.extend(["--token", token])
if scope:
cmd.extend(["--scope", scope])
# Add the secret
cmd.extend(["--env", "DATASETTE_SECRET={}".format(secret)])
run(cmd)


@hookimpl
def publish_subcommand(publish):
@publish.command()
@add_common_publish_arguments_and_options
@add_vercel_options
def vercel(*args, **kwargs):
"Publish to https://vercel.com/"
_publish_vercel(*args, **kwargs)

@publish.command()
@add_common_publish_arguments_and_options
@add_vercel_options
def now(*args, **kwargs):
"Alias for 'datasette publish vercel'"
_publish_vercel(*args, **kwargs)
14 changes: 14 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "datasette-nteract-data-explorer",
"version": 2,
"builds": [
{
"src": "index.py",
"use": "@vercel/python"
}
],
"functions": {
"index.py"
},
"rewrites": [{ "src": "/(.*)", "dest": "index.py" }]
}

0 comments on commit 1fb4646

Please sign in to comment.