Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): Serverless output for static websites and Next.js #135

Merged
merged 6 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/otiai10/copy v1.12.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/moznion/go-optional v0.10.0 h1:YE42pzLDp6vc9zi/2hyaHYJesjahZEgFXEN1u5DMwMA=
github.com/moznion/go-optional v0.10.0/go.mod h1:l3mLmsyp2bWTvWKjEm5MT7lo3g5MRlNIflxFB0XTASA=
github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=
github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
github.com/pan93412/envexpander v1.1.0 h1:cf57P8BllM2CWfHO0eEKkpIoz0e44J94+H2Mh+hcUtw=
github.com/pan93412/envexpander v1.1.0/go.mod h1:sBLOiYNNmCNyx+6z6L3Cegcs1MG0l7KXu8rhe41ZsM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
Expand Down
51 changes: 3 additions & 48 deletions internal/nodejs/__snapshots__/template_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ RUN yarn install
# Build if we can build it



EXPOSE 8080
CMD yarn start


---

[TestTemplate_NBuildCmd_OutputDir_NSPA - 1]
Expand All @@ -34,21 +32,8 @@ RUN yarn install
# Build if we can build it



FROM nginx:alpine as runtime

COPY --from=build /src/dist /usr/share/nginx/html/static
RUN echo "\
server { \
listen 8080; \
root /usr/share/nginx/html/static; \
absolute_redirect off; \
location / { \
try_files \$uri \$uri.html \$uri/index.html /404.html =404; \
} \
}"> /etc/nginx/conf.d/default.conf
EXPOSE 8080

CMD yarn start

---

Expand All @@ -66,21 +51,8 @@ RUN yarn install
# Build if we can build it



FROM nginx:alpine as runtime

COPY --from=build /src/dist /usr/share/nginx/html/static
RUN echo "\
server { \
listen 8080; \
root /usr/share/nginx/html/static; \
absolute_redirect off; \
location / { \
try_files \$uri /index.html; \
} \
}"> /etc/nginx/conf.d/default.conf
EXPOSE 8080

CMD yarn start

---

Expand All @@ -98,11 +70,9 @@ RUN yarn install
# Build if we can build it
RUN yarn build


EXPOSE 8080
CMD yarn start


---

[TestTemplate_BuildCmd_OutputDir - 1]
Expand All @@ -119,21 +89,8 @@ RUN yarn install
# Build if we can build it
RUN yarn build


FROM nginx:alpine as runtime

COPY --from=build /src/dist /usr/share/nginx/html/static
RUN echo "\
server { \
listen 8080; \
root /usr/share/nginx/html/static; \
absolute_redirect off; \
location / { \
try_files \$uri /index.html; \
} \
}"> /etc/nginx/conf.d/default.conf
EXPOSE 8080

CMD yarn start

---

Expand All @@ -157,9 +114,7 @@ RUN bun install
# Build if we can build it



EXPOSE 8080
CMD bun start main.ts


---
86 changes: 86 additions & 0 deletions internal/nodejs/nextjs/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package nextjs

import (
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strings"

cp "github.com/otiai10/copy"
)

// constructNextFunction will construct the first function page, used as symlinks for other function pages
func constructNextFunction(zeaburOutputDir, firstFuncPage string) error {
p := path.Join(zeaburOutputDir, "functions", firstFuncPage+".func")

err := os.MkdirAll(p, 0755)
if err != nil {
return fmt.Errorf("create function dir: %w", err)
}

launcher, err := renderLauncher()
if err != nil {
return fmt.Errorf("render launcher: %w", err)
}

err = os.WriteFile(path.Join(p, "index.js"), []byte(launcher), 0644)
if err != nil {
return fmt.Errorf("write launcher: %w", err)
}

err = cp.Copy(".next", path.Join(p, ".next"))
if err != nil {
return fmt.Errorf("copy .next: %w", err)
}

err = cp.Copy("package.json", path.Join(p, "package.json"))
if err != nil {
return fmt.Errorf("copy package.json: %w", err)
}

outputNodeModulesDir := path.Join(p, "node_modules")
err = os.MkdirAll(outputNodeModulesDir, 0755)
if err != nil {
return fmt.Errorf("create node_modules dir: %w", err)
}

var deps []string
err = filepath.Walk(".next", func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, ".nft.json") {
type nftJSON struct {
Files []string `json:"files"`
}
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read nft.json: %w", err)
}
var nft nftJSON
err = json.Unmarshal(b, &nft)
if err != nil {
return fmt.Errorf("unmarshal nft.json: %w", err)
}
for _, file := range nft.Files {
if !strings.Contains(file, "node_modules") {
continue
}
file = file[strings.Index(file, "node_modules"):]
deps = append(deps, file)
}
}
return nil
})
if err != nil {
return fmt.Errorf("walk .next: %w", err)
}

for _, dep := range deps {
err = cp.Copy(dep, path.Join(p, dep))
if err != nil {
return fmt.Errorf("copy dep: %w", err)
}
}

return nil
}
68 changes: 68 additions & 0 deletions internal/nodejs/nextjs/launcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package nextjs

import (
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"text/template"
)

//go:embed launcher.js.tmpl
var launcherTemplate string

// getNextConfig read .next/required-server-files.json and return the config string that will be injected into launcher
func getNextConfig() (string, error) {
rsf, err := os.ReadFile(".next/required-server-files.json")
if err != nil {
return "", fmt.Errorf("read required-server-files.json: %w", err)
}

type requiredServerFiles struct {
Config json.RawMessage `json:"config"`
}

var rs requiredServerFiles
err = json.Unmarshal(rsf, &rs)
if err != nil {
return "", fmt.Errorf("unmarshal required-server-files.json: %w", err)
}

var data map[string]interface{}
if err := json.Unmarshal(rs.Config, &data); err != nil {
return "", fmt.Errorf("unmarshal config: %w", err)
}

nextConfig, err := json.Marshal(data)
if err != nil {
return "", fmt.Errorf("marshal config: %w", err)
}

return string(nextConfig), nil
}

// renderLauncher will render the launcher.js template which used as the entrypoint of the serverless function
func renderLauncher() (string, error) {
nextConfig, err := getNextConfig()
if err != nil {
return "", fmt.Errorf("get next config: %w", err)
}

tmpl, err := template.New("launcher").Parse(launcherTemplate)
if err != nil {
return "", fmt.Errorf("parse launcher template: %w", err)
}

type renderLauncherTemplateContext struct {
NextConfig string
}

var launcher strings.Builder
err = tmpl.Execute(&launcher, renderLauncherTemplateContext{NextConfig: nextConfig})
if err != nil {
return "", fmt.Errorf("render launcher template: %w", err)
}

return launcher.String(), nil
}
24 changes: 24 additions & 0 deletions internal/nodejs/nextjs/launcher.js.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = "next"
const NextServer = require('next/dist/server/next-server.js').default;
const nextServer = new NextServer({
conf: {{ .NextConfig }},
dir: '.',
minimalMode: true,
customServer: false,
});
const requestHandler = nextServer.getRequestHandler();
module.exports = async (req, res) => {
const { NodeNextRequest, NodeNextResponse} = require('next/dist/server/base-http/node');
req = new NodeNextRequest(req)
res = new NodeNextResponse(res)
try {
await requestHandler(req, res);
} catch (err) {
console.error(err);
process.exit(1);
}
};
Loading