Skip to content

Commit

Permalink
feat(lambda-at-edge): detect and copy next-i18next files (#1472)
Browse files Browse the repository at this point in the history
  • Loading branch information
dphang committed Jul 31, 2021
1 parent a5d74d2 commit 3ffc938
Show file tree
Hide file tree
Showing 40 changed files with 252 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/libs/lambda-at-edge/src/build.ts
Expand Up @@ -20,6 +20,7 @@ import { Item } from "klaw";
import { Job } from "@vercel/nft/out/node-file-trace";
import { prepareBuildManifests } from "@sls-next/core";
import { NextConfig } from "@sls-next/core/dist/build";
import { NextI18nextIntegration } from "./build/third-party/next-i18next";

export const DEFAULT_LAMBDA_CODE_DIR = "default-lambda";
export const API_LAMBDA_CODE_DIR = "api-lambda";
Expand Down Expand Up @@ -331,6 +332,9 @@ class Builder {
this.processAndCopyRoutesManifest(
join(this.dotNextDir, "routes-manifest.json"),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, "routes-manifest.json")
),
this.runThirdPartyIntegrations(
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR)
)
]);
}
Expand Down Expand Up @@ -785,6 +789,17 @@ class Builder {
// Copy static assets to .serverless_nextjs directory
await this.buildStaticAssets(defaultBuildManifest, routesManifest);
}

/**
* Run additional integrations for third-party libraries such as next-i18next.
* These are usually needed to add additional files into the lambda, etc.
* @param outputLambdaDir
*/
async runThirdPartyIntegrations(outputLambdaDir: string): Promise<void> {
await Promise.all([
new NextI18nextIntegration(this.nextConfigDir, outputLambdaDir).execute()
]);
}
}

export default Builder;
3 changes: 3 additions & 0 deletions packages/libs/lambda-at-edge/src/build/third-party/README.md
@@ -0,0 +1,3 @@
## Third party integrations

This will support common third-party integrations such as next-i18next. The integrations here are meant to auto-detect what your project is using and copy the appropriate files to the build artifacts.
@@ -0,0 +1,25 @@
import { join } from "path";
import fse from "fs-extra";

export abstract class ThirdPartyIntegrationBase {
nextConfigDir: string;
outputLambdaDir: string;

constructor(nextConfigDir: string, outputLambdaDir: string) {
this.nextConfigDir = nextConfigDir;
this.outputLambdaDir = outputLambdaDir;
}

abstract execute(): void;

async isPackagePresent(name: string): Promise<boolean> {
const packageJsonPath = join(this.nextConfigDir, "package.json");

if (await fse.pathExists(packageJsonPath)) {
const packageJson = await fse.readJSON(packageJsonPath);
return !!packageJson.dependencies[name];
}

return false;
}
}
32 changes: 32 additions & 0 deletions packages/libs/lambda-at-edge/src/build/third-party/next-i18next.ts
@@ -0,0 +1,32 @@
import fse from "fs-extra";
import { join } from "path";
import { ThirdPartyIntegrationBase } from "./integration-base";

export class NextI18nextIntegration extends ThirdPartyIntegrationBase {
/**
* This will copy all next-i18next files as needed to a lambda directory.
*/
async execute(): Promise<void> {
if (await this.isPackagePresent("next-i18next")) {
const localeSrc = join(this.nextConfigDir, "public", "locales");
const localeDest = join(this.outputLambdaDir, "public", "locales");

if (await fse.pathExists(localeSrc)) {
await fse.copy(localeSrc, localeDest, { recursive: true });
}

const nextI18nextConfigSrc = join(
this.nextConfigDir,
"next-i18next.config.js"
);
const nextI18nextConfigDest = join(
this.outputLambdaDir,
"next-i18next.config.js"
);

if (await fse.pathExists(nextI18nextConfigSrc)) {
await fse.copy(nextI18nextConfigSrc, nextI18nextConfigDest);
}
}
}
}
@@ -0,0 +1,83 @@
import { join } from "path";
import fse from "fs-extra";
import execa from "execa";
import Builder, {
DEFAULT_LAMBDA_CODE_DIR,
API_LAMBDA_CODE_DIR,
IMAGE_LAMBDA_CODE_DIR
} from "../../src/build";
import { cleanupDir } from "../test-utils";
import {
OriginRequestDefaultHandlerManifest,
OriginRequestApiHandlerManifest,
OriginRequestImageHandlerManifest
} from "../../src/types";

jest.mock("execa");

describe("Builder Tests (with third party integrations)", () => {
let fseRemoveSpy: jest.SpyInstance;
let fseEmptyDirSpy: jest.SpyInstance;
let defaultBuildManifest: OriginRequestDefaultHandlerManifest;
let apiBuildManifest: OriginRequestApiHandlerManifest;
let imageBuildManifest: OriginRequestImageHandlerManifest;

const fixturePath = join(
__dirname,
"./simple-app-fixture-third-party-integrations"
);
const outputDir = join(fixturePath, ".test_sls_next_output");

describe("Regular build", () => {
beforeEach(async () => {
const mockExeca = execa as jest.Mock;
mockExeca.mockResolvedValueOnce();

fseRemoveSpy = jest.spyOn(fse, "remove").mockImplementation(() => {
return;
});
fseEmptyDirSpy = jest.spyOn(fse, "emptyDir");

const builder = new Builder(fixturePath, outputDir, {});
await builder.build();

defaultBuildManifest = await fse.readJSON(
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/manifest.json`)
);

apiBuildManifest = await fse.readJSON(
join(outputDir, `${API_LAMBDA_CODE_DIR}/manifest.json`)
);

imageBuildManifest = await fse.readJSON(
join(outputDir, `${IMAGE_LAMBDA_CODE_DIR}/manifest.json`)
);
});

afterEach(() => {
fseEmptyDirSpy.mockRestore();
fseRemoveSpy.mockRestore();
//return cleanupDir(outputDir);
});

describe("Default Handler Third Party Files", () => {
it("next-i18next files are copied", async () => {
expect(
await fse.pathExists(
join(
outputDir,
`${DEFAULT_LAMBDA_CODE_DIR}`,
"next-i18next.config.js"
)
)
).toBe(true);

const localesFiles = await fse.readdir(
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}`, "public", "locales")
);

expect(localesFiles).toEqual(expect.arrayContaining(["de", "en"]));
});
});
});
});
@@ -0,0 +1 @@
test-build-id
@@ -0,0 +1 @@
Having a cache/ folder allows to test the cleanup of .next/ except for cache/ for faster builds
@@ -0,0 +1,28 @@
{
"version": 1,
"images": {
"deviceSizes": [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
"imageSizes": [16, 32, 48, 64, 96, 128, 256, 384],
"domains": [],
"path": "/_next/image",
"loader": "default",
"sizes": [
640,
750,
828,
1080,
1200,
1920,
2048,
3840,
16,
32,
48,
64,
96,
128,
256,
384
]
}
}
@@ -0,0 +1,16 @@
{
"version": 2,
"routes": {
"/": {
"initialRevalidateSeconds": false,
"srcRoute": null,
"dataRoute": "/_next/data/zsWqBqLjpgRmswfQomanp/index.json"
},
"/contact": {
"initialRevalidateSeconds": false,
"srcRoute": null,
"dataRoute": "/_next/data/zsWqBqLjpgRmswfQomanp/contact.json"
}
},
"dynamicRoutes": {}
}
@@ -0,0 +1 @@
{"version":1,"pages404":true,"basePath":"","redirects":[],"rewrites":[],"headers":[],"dynamicRoutes":[]}
@@ -0,0 +1,19 @@
{
"/[root]": "pages/[root].js",
"/customers/[customer]": "pages/customers/[customer].js",
"/customers/[customer]/[post]": "pages/customers/[customer]/[post].js",
"/customers/new": "pages/customers/new.js",
"/customers/[customer]/profile": "pages/customers/[customer]/profile.js",
"/customers/[...catchAll]": "pages/customers/[...catchAll].js",
"/products/[[...optionalCatchAll]]": "pages/products/[[...optionalCatchAll]].js",
"/api/customers": "pages/api/customers.js",
"/api/customers/[id]": "pages/api/customers/[id].js",
"/api/customers/new": "pages/api/customers/new.js",
"/terms": "pages/terms.html",
"/about": "pages/about.html",
"/blog/[post]": "pages/blog/[post].html",
"/": "pages/index.js",
"/_app": "pages/_app.js",
"/_document": "pages/_document.js",
"/404": "pages/404.html"
}
@@ -0,0 +1,9 @@
const path = require("path");

module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "de"],
localePath: path.resolve("./public/locales")
}
};
@@ -0,0 +1 @@
module.exports = { target: "serverless" };
@@ -0,0 +1,5 @@
{
"dependencies": {
"next-i18next": "^8.5.5"
}
}
@@ -0,0 +1,6 @@
{
"button": {
"login": "Login",
"register": "Register"
}
}
@@ -0,0 +1,6 @@
{
"button": {
"login": "Login",
"register": "Register"
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1 @@
// Test handler file to check if copied

0 comments on commit 3ffc938

Please sign in to comment.