Skip to content

Commit

Permalink
Add purge api
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed May 1, 2024
1 parent 989c321 commit 306c020
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 75 deletions.
4 changes: 2 additions & 2 deletions config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
// The log level, default is "info", you can also set it to "debug" to enable debug logs.
"logLevel": "info",

// The origin of CDN, default is using the origin of the request.
// The origin of the CDN, default is using the origin of the request.
// Use to fix origin with reverse proxy, for examle "https://esm.sh"
"cdnOrigin": "",

// The base path of CDN, default is "/".
// The base path of the CDN, default is "/".
"cdnBasePath": "/",

// The npm registry, default is "https://registry.npmjs.org/".
Expand Down
70 changes: 52 additions & 18 deletions packages/esm-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,20 +354,6 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d
pathname = pathname.slice(0, -1);
}

if (req.method === "POST" && pathname === "/transform") {
return await fetchOrigin(
new Request(req.url, {
method: "POST",
headers: req.headers,
body: req.body,
}),
env,
ctx,
"/transform",
corsHeaders(),
);
}

switch (pathname) {
case "/error.js":
return ctx.withCache(() => fetchOrigin(req, env, ctx, pathname + url.search, corsHeaders()));
Expand Down Expand Up @@ -429,17 +415,65 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d
}
}

if (req.method === "POST") {
if (pathname === "/transform" || pathname === "/purge") {
const res = await fetchOrigin(
new Request(req.url, {
method: "POST",
headers: req.headers,
body: req.body,
}),
env,
ctx,
pathname,
corsHeaders(),
);
if (pathname === "/purge") {
const deletedFiles: string[] = await res.json();
const headers = new Headers(res.headers);
if (deletedFiles.length > 0) {
const KV = Reflect.get(env, "KV") as KVNamespace | undefined;
const R2 = Reflect.get(env, "R2") as R2Bucket | undefined;
if (R2?.delete) {
// delete the source map files in R2 storage
await R2.delete(deletedFiles.filter((k) => !k.endsWith(".css")).map((k) => k + ".map"));
}
if (KV?.delete && deletedFiles.length <= 42) {
await Promise.all(deletedFiles.map((k) => KV.delete(k)));
} else {
headers.set("X-KV-Purged", "false");
}
}
headers.delete("Content-Length");
return new Response(JSON.stringify(deletedFiles), { headers });
}
return res;
} else if (pathname === "/purge-kv") {
const keys = await req.json();
if (!Array.isArray(keys) || keys.length === 0 || keys.length > 42) {
return err("No keys or too many keys", 400);
}
const KV = Reflect.get(env, "KV") as KVNamespace | undefined;
if (!KV?.delete) {
return err("KV namespace not found", 500);
}
await Promise.all(keys.map((k) => KV.delete(k)));
return new Response(`Deleted ${keys.length} keys`, { status: 200 });
}
return err("Not Found", 404);
}

if (req.method !== "GET" && req.method !== "HEAD") {
return err("Method Not Allowed", 405);
}

// return 404 for favicon.ico and robots.txt
if (pathname === "/favicon.ico" || pathname === "/robots.txt") {
// return 404 for robots.txt
if (pathname === "/robots.txt") {
return err("Not Found", 404);
}

// use the default landing page/embedded files
if (pathname === "/" || pathname.startsWith("/embed/")) {
if (pathname === "/" || pathname === "/favicon.ico" || pathname.startsWith("/embed/")) {
return fetchOrigin(req, env, ctx, `${pathname}${url.search}`, corsHeaders());
}

Expand Down Expand Up @@ -753,7 +787,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d
prefix += "/gh";
}

if (isDtsFile(subPath) || isTargetUrl) {
if (isTargetUrl || isDtsFile(subPath)) {
return ctx.withCache(() => {
const pathname = `${prefix}/${pkgId}@${packageVersion}${subPath}`;
return fetchOriginWithKVCache(req, env, ctx, pathname, undefined, true);
Expand Down
2 changes: 0 additions & 2 deletions server/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ type ESMBuild struct {
type BuildTask struct {
Args BuildArgs
Pkg Pkg
CdnOrigin string
Target string
Dev bool
Bundle bool
Expand Down Expand Up @@ -1134,7 +1133,6 @@ func (task *BuildTask) resolveExternalModule(specifier string, kind api.ResolveK
subBuild := &BuildTask{
Args: task.Args,
Pkg: subPkg,
CdnOrigin: task.CdnOrigin,
Target: task.Target,
Dev: task.Dev,
Bundle: task.Bundle,
Expand Down
8 changes: 4 additions & 4 deletions server/build_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,14 +641,14 @@ func (task *BuildTask) resolveConditions(p *NpmPackageInfo, exports interface{},
func queryESMBuild(id string) (*ESMBuild, bool) {
value, err := db.Get(id)
if err == nil && value != nil {
var esm ESMBuild
err = json.Unmarshal(value, &esm)
var esmb ESMBuild
err = json.Unmarshal(value, &esmb)
if err == nil {
if !esm.TypesOnly {
if !esmb.TypesOnly {
_, err = fs.Stat(path.Join("builds", id))
}
if err == nil || os.IsExist(err) {
return &esm, true
return &esmb, true
}
}
// delete the invalid db entry
Expand Down
17 changes: 15 additions & 2 deletions server/dts_transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (task *BuildTask) transformDTS(dts string, aliasDepsPrefix string, marker *
pkgNameWithVersion,
aliasDepsPrefix,
}, strings.Split(subPath, "/")...), "/"))
savePath := normalizeSavePath(path.Join("types", getTypesRoot(task.CdnOrigin), dtsPath))
savePath := normalizeSavePath(path.Join("types", dtsPath))
_, err = fs.Stat(savePath)
if err != nil && err != storage.ErrNotFound {
return
Expand Down Expand Up @@ -92,11 +92,24 @@ func (task *BuildTask) transformDTS(dts string, aliasDepsPrefix string, marker *
internalDeclModules.Add(path)
}

origin := cfg.CdnOrigin
if origin == "" {
port := cfg.Port
if port == 0 {
port = 8080
}
if port == 80 {
origin = "http://localhost"
} else {
origin = fmt.Sprintf("http://localhost:%d", port)
}
}

resolveDir := task.resolveDir
buf := bytes.NewBuffer(nil)
footer := bytes.NewBuffer(nil)
imports := newStringSet()
dtsBasePath := task.CdnOrigin + cfg.CdnBasePath
dtsBasePath := origin + cfg.CdnBasePath

if pkgName == "@types/node" {
fmt.Fprintf(buf, "/// <reference path=\"%s/node.ns.d.ts\" />\n", dtsBasePath)
Expand Down
85 changes: 53 additions & 32 deletions server/esm_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"sort"
Expand Down Expand Up @@ -97,7 +96,7 @@ func esmHandler() rex.Handle {
savePath := fmt.Sprintf("modules/+%s.%s.mjs", hash, input.Target)
_, err = fs.Stat(savePath)
if err == nil {
r, err := fs.OpenFile(savePath)
r, err := fs.Open(savePath)
if err != nil {
return rex.Err(500, "failed to read code")
}
Expand All @@ -123,7 +122,40 @@ func esmHandler() rex.Handle {
"code": code,
}
case "/purge":
return "TODO: purge"
packageName := ctx.Form.Value("package")
version := ctx.Form.Value("version")
github := ctx.Form.Has("github")
if packageName == "" {
return rex.Err(400, "packageName is required")
}
prefix := packageName + "@"
if github {
prefix = fmt.Sprintf("gh/%s", packageName)
}
if version != "" {
prefix += version
}
deletedKVs, err := db.DeleteAll(prefix)
if err != nil {
return rex.Err(500, err.Error())
}
ret := []string{}
for _, kv := range deletedKVs {
var esmb ESMBuild
key := string(kv[0])
if json.Unmarshal(kv[1], &esmb) == nil {
savePath := fmt.Sprintf("builds/%s", key)
go fs.Remove(savePath)
go fs.Remove(savePath + ".map")
if esmb.PackageCSS {
cssKey := strings.TrimSuffix(key, path.Ext(key)) + ".css"
go fs.Remove(fmt.Sprintf("builds/%s", cssKey))
ret = append(ret, cssKey)
}
ret = append(ret, key)
}
}
return ret
default:
return rex.Err(404, "not found")
}
Expand Down Expand Up @@ -318,22 +350,22 @@ func esmHandler() rex.Handle {
return rex.Status(404, "not found")
}
target := getBuildTargetByUA(userAgent)
savaPath := fmt.Sprintf("modules/+%s.%s.%s", hash, target, ext)
fi, err := fs.Stat(savaPath)
savePath := fmt.Sprintf("modules/+%s.%s.%s", hash, target, ext)
fi, err := fs.Stat(savePath)
if err != nil {
if err == storage.ErrNotFound {
return rex.Status(404, "not found")
}
return rex.Status(500, err.Error())
}
r, err := fs.OpenFile(savaPath)
r, err := fs.Open(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
header.Set("Content-Type", ctJavascript)
header.Set("Cache-Control", ccImmutable)
addVary(header, "User-Agent")
return rex.Content(savaPath, fi.ModTime(), r) // auto closed
return rex.Content(savePath, fi.ModTime(), r) // auto closed
}

// serve node libs
Expand Down Expand Up @@ -671,7 +703,7 @@ func esmHandler() rex.Handle {
if isTargetUrl && (resType == "build" || resType == "types") {
savePath := path.Join("builds", pathname)
if resType == "types" {
savePath = path.Join("types", getTypesRoot(cdnOrigin), strings.TrimPrefix(savePath, "types/"))
savePath = path.Join("types", pathname)
}
savePath = normalizeSavePath(savePath)
fi, err := fs.Stat(savePath)
Expand Down Expand Up @@ -700,7 +732,7 @@ func esmHandler() rex.Handle {
moduleUrl,
)
}
r, err := fs.OpenFile(savePath)
r, err := fs.Open(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
Expand Down Expand Up @@ -840,8 +872,7 @@ func esmHandler() rex.Handle {
if resType == "types" {
findDts := func() (savePath string, fi storage.FileStat, err error) {
savePath = path.Join(fmt.Sprintf(
"types/%s%s/%s@%s/%s",
getTypesRoot(cdnOrigin),
"types%s/%s@%s/%s",
ghPrefix,
reqPkg.Name,
reqPkg.Version,
Expand All @@ -865,10 +896,9 @@ func esmHandler() rex.Handle {
_, _, err := findDts()
if err == storage.ErrNotFound {
task := &BuildTask{
Args: buildArgs,
CdnOrigin: cdnOrigin,
Pkg: reqPkg,
Target: "types",
Args: buildArgs,
Pkg: reqPkg,
Target: "types",
}
c := buildQueue.Add(task, ctx.RemoteIP())
select {
Expand All @@ -889,7 +919,7 @@ func esmHandler() rex.Handle {
}
return rex.Status(500, err.Error())
}
r, err := fs.OpenFile(savePath)
r, err := fs.Open(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
Expand Down Expand Up @@ -960,13 +990,12 @@ func esmHandler() rex.Handle {

// build and return the module
task := &BuildTask{
Args: buildArgs,
CdnOrigin: cdnOrigin,
Pkg: reqPkg,
Target: target,
Dev: isDev,
Bundle: bundle,
NoBundle: noBundle,
Args: buildArgs,
Pkg: reqPkg,
Target: target,
Dev: isDev,
Bundle: bundle,
NoBundle: noBundle,
}
buildId := task.ID()
esm, hasBuild := queryESMBuild(buildId)
Expand Down Expand Up @@ -1034,7 +1063,7 @@ func esmHandler() rex.Handle {
}
return rex.Status(500, err.Error())
}
f, err := fs.OpenFile(savePath)
f, err := fs.Open(savePath)
if err != nil {
return rex.Status(500, err.Error())
}
Expand Down Expand Up @@ -1153,11 +1182,3 @@ func throwErrorJS(ctx *rex.Context, message string, static bool) interface{} {
ctx.W.Header().Set("Content-Type", ctJavascript)
return rex.Status(500, buf)
}

func getTypesRoot(cdnOrigin string) string {
url, err := url.Parse(cdnOrigin)
if err != nil {
return "-"
}
return strings.ReplaceAll(url.Host, ":", "_")
}
1 change: 1 addition & 0 deletions server/storage/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type DataBase interface {
Get(key string) ([]byte, error)
Put(key string, value []byte) error
Delete(key string) error
DeleteAll(prefix string) ([][2][]byte, error)
Close() error
}

Expand Down
Loading

0 comments on commit 306c020

Please sign in to comment.