Skip to content

Commit

Permalink
fix: #7696 - support production package install for function category
Browse files Browse the repository at this point in the history
  • Loading branch information
attilah committed Jul 27, 2021
1 parent 57eeae5 commit 8eb650e
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 20 deletions.
7 changes: 5 additions & 2 deletions packages/amplify-category-function/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,12 @@ export async function getInvoker(
}

export function getBuilder(context: $TSContext, resourceName: string, buildType: BuildType): () => Promise<void> {
const lastBuildTimestamp = _.get(stateManager.getMeta(), [categoryName, resourceName, buildTypeKeyMap[buildType]]);
const meta = stateManager.getMeta();
const lastBuildTimestamp = _.get(meta, [categoryName, resourceName, buildTypeKeyMap[buildType]]);
const lastBuildType = _.get(meta, [categoryName, resourceName, 'lastBuildType']);

return async () => {
await buildFunction(context, { resourceName, buildType, lastBuildTimestamp });
await buildFunction(context, { resourceName, buildType, lastBuildTimestamp, lastBuildType });
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { categoryName } from '../../../constants';

export const buildFunction = async (
context: $TSContext,
{ resourceName, lastBuildTimestamp, buildType = BuildType.PROD }: BuildRequestMeta,
{ resourceName, lastBuildTimestamp, lastBuildType, buildType = BuildType.PROD }: BuildRequestMeta,
) => {
const resourcePath = path.join(pathManager.getBackendDirPath(), categoryName, resourceName);
const breadcrumbs = context.amplify.readBreadcrumbs(categoryName, resourceName);
Expand Down Expand Up @@ -35,9 +35,10 @@ export const buildFunction = async (
runtime: breadcrumbs.functionRuntime,
legacyBuildHookParams: {
projectRoot: pathManager.findProjectRoot(),
resourceName: resourceName,
resourceName,
},
lastBuildTimeStamp: prevBuildTime,
lastBuildType,
};
rebuilt = (await runtimePlugin.build(buildRequest)).rebuilt;
}
Expand All @@ -52,6 +53,7 @@ export const buildFunction = async (
export interface BuildRequestMeta {
resourceName: string;
lastBuildTimestamp?: string;
lastBuildType?: BuildType;
buildType?: BuildType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export async function updateamplifyMetaAfterPush(resources: $TSObject[]) {
export function updateamplifyMetaAfterBuild({ category, resourceName }: ResourceTuple, buildType: BuildType = BuildType.PROD) {
const amplifyMeta = stateManager.getMeta();
_.set(amplifyMeta, [category, resourceName, buildTypeKeyMap[buildType]], new Date());
_.set(amplifyMeta, [category, resourceName, 'lastBuildType'], buildType);
stateManager.setMeta(undefined, amplifyMeta);
}

Expand Down
8 changes: 5 additions & 3 deletions packages/amplify-e2e-core/src/init/deleteProject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { nspawn as spawn, retry, getCLIPath, describeCloudFormationStack, getProjectMeta } from '..';
import { nspawn as spawn, retry, getCLIPath, describeCloudFormationStack } from '..';
import { getBackendAmplifyMeta } from '../utils';

export const deleteProject = async (cwd: string, profileConfig?: any): Promise<void> => {
const { StackName: stackName, Region: region } = getProjectMeta(cwd).providers.awscloudformation;
// Read the meta from backend otherwise it could fail on non-pushed, just initialized projects
const { StackName: stackName, Region: region } = getBackendAmplifyMeta(cwd).providers.awscloudformation;
await retry(
() => describeCloudFormationStack(stackName, region, profileConfig),
stack => stack.StackStatus.endsWith('_COMPLETE'),
Expand All @@ -10,7 +12,7 @@ export const deleteProject = async (cwd: string, profileConfig?: any): Promise<v
const noOutputTimeout = 1000 * 60 * 20; // 20 minutes;
spawn(getCLIPath(), ['delete'], { cwd, stripColors: true, noOutputTimeout })
.wait('Are you sure you want to continue?')
.sendLine('y')
.sendConfirmYes()
.wait('Project deleted locally.')
.run((err: Error) => {
if (!err) {
Expand Down
80 changes: 79 additions & 1 deletion packages/amplify-e2e-tests/src/__tests__/function_3.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { initJSProjectWithProfile, deleteProject, amplifyPushAuth } from 'amplify-e2e-core';
import { initJSProjectWithProfile, deleteProject, amplifyPushAuth, getBackendAmplifyMeta } from 'amplify-e2e-core';
import { addFunction, functionMockAssert, functionCloudInvoke } from 'amplify-e2e-core';
import { createNewProjectDir, deleteProjectDir } from 'amplify-e2e-core';
import _ from 'lodash';

describe('go function tests', () => {
const helloWorldSuccessOutput = 'Hello Amplify!';
Expand Down Expand Up @@ -152,3 +153,80 @@ describe('dotnet function tests', () => {
expect(JSON.parse(response.Payload.toString())).toEqual(helloWorldSuccessObj);
});
});

describe('nodejs function tests', () => {
const helloWorldSuccessString = 'Hello from Lambda!';
const helloWorldSuccessObj = {
statusCode: 200,
body: '"Hello from Lambda!"',
};

let projRoot: string;
let funcName: string;

beforeEach(async () => {
projRoot = await createNewProjectDir('nodejs-functions');
await initJSProjectWithProfile(projRoot, {});

const random = Math.floor(Math.random() * 10000);
funcName = `nodejstestfn${random}`;

await addFunction(
projRoot,
{
name: funcName,
functionTemplate: 'Hello World',
},
'nodejs',
);
});

afterEach(async () => {
await deleteProject(projRoot);
deleteProjectDir(projRoot);
});

it('add nodejs hello world function and mock locally', async () => {
await functionMockAssert(projRoot, {
funcName,
successString: helloWorldSuccessString,
eventFile: 'src/event.json',
}); // will throw if successString is not in output
});

it('add nodejs hello world function and invoke in the cloud', async () => {
const payload = '{"key1":"value1","key2":"value2","key3":"value3"}';

await amplifyPushAuth(projRoot);

const response = await functionCloudInvoke(projRoot, { funcName, payload });

expect(JSON.parse(response.Payload.toString())).toEqual(helloWorldSuccessObj);
});

it('add nodejs hello world function and mock locally, check buildType, push, check buildType', async () => {
await functionMockAssert(projRoot, {
funcName,
successString: helloWorldSuccessString,
eventFile: 'src/event.json',
}); // will throw if successString is not in output

let meta = getBackendAmplifyMeta(projRoot);
let functionResource = _.get(meta, ['function', funcName]);

const lastDevBuildTimeStampBeforePush = functionResource.lastDevBuildTimeStamp;

// Mock should trigger a DEV build of the function
expect(functionResource).toBeDefined();
expect(functionResource.lastBuildType).toEqual('DEV');

await amplifyPushAuth(projRoot);

meta = getBackendAmplifyMeta(projRoot);
functionResource = _.get(meta, ['function', funcName]);

// Push should trigger a PROD build of the function
expect(functionResource.lastBuildType).toEqual('PROD');
expect(functionResource.lastDevBuildTimeStamp).toEqual(lastDevBuildTimeStampBeforePush);
});
});
1 change: 1 addition & 0 deletions packages/amplify-function-plugin-interface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type BuildRequest = {
resourceName: string;
};
lastBuildTimeStamp?: Date;
lastBuildType?: BuildType;
service?: string;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,25 @@ describe('legacy build resource', () => {
srcRoot: 'resourceDir',
runtime: 'other',
buildType: BuildType.PROD,
lastBuildType: BuildType.PROD,
});

expect(result.rebuilt).toEqual(false);
expect(glob_mock.sync.mock.calls.length).toBe(1);
expect(fs_mock.statSync.mock.calls.length).toBe(5);
});

it('checks lastBuildType difference triggers rebuild', async () => {
glob_mock.sync.mockImplementationOnce(() => Array.from(stubFileTimestamps.keys()));
fs_mock.statSync.mockImplementation(file => ({ mtime: new Date(stubFileTimestamps.get(file.toString())!) } as any));

const result = await buildResource({
lastBuildTimeStamp: new Date(timestamp),
srcRoot: 'resourceDir',
runtime: 'other',
buildType: BuildType.PROD,
});

expect(result.rebuilt).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getPackageManager } from 'amplify-cli-core';
import { BuildRequest, BuildResult } from 'amplify-function-plugin-interface';
import { BuildRequest, BuildResult, BuildType } from 'amplify-function-plugin-interface';
import execa from 'execa';
import * as fs from 'fs-extra';
import glob from 'glob';
Expand All @@ -9,8 +9,8 @@ import * as path from 'path';
export async function buildResource(request: BuildRequest): Promise<BuildResult> {
const resourceDir = request.service ? request.srcRoot : path.join(request.srcRoot, 'src');

if (!request.lastBuildTimeStamp || isBuildStale(request.srcRoot, request.lastBuildTimeStamp)) {
installDependencies(resourceDir);
if (!request.lastBuildTimeStamp || isBuildStale(request.srcRoot, request.lastBuildTimeStamp, request.buildType, request.lastBuildType)) {
installDependencies(resourceDir, request.buildType);
if (request.legacyBuildHookParams) {
runBuildScriptHook(request.legacyBuildHookParams.resourceName, request.legacyBuildHookParams.projectRoot);
}
Expand All @@ -22,24 +22,25 @@ export async function buildResource(request: BuildRequest): Promise<BuildResult>
function runBuildScriptHook(resourceName: string, projectRoot: string) {
const scriptName = `amplify:${resourceName}`;
if (scriptExists(projectRoot, scriptName)) {
runPackageManager(projectRoot, scriptName);
runPackageManager(projectRoot, undefined, scriptName);
}
}

function scriptExists(projectRoot: string, scriptName: string) {
const packageJsonPath = path.normalize(path.join(projectRoot, 'package.json'));
if (fs.existsSync(packageJsonPath)) {
const rootPackageJsonContents = require(packageJsonPath);

return rootPackageJsonContents.scripts && rootPackageJsonContents.scripts[scriptName];
}
return false;
}

function installDependencies(resourceDir: string) {
runPackageManager(resourceDir);
function installDependencies(resourceDir: string, buildType: BuildType) {
runPackageManager(resourceDir, buildType);
}

function runPackageManager(cwd: string, scriptName?: string) {
function runPackageManager(cwd: string, buildType?: BuildType, scriptName?: string) {
const packageManager = getPackageManager(cwd);

if (packageManager === null) {
Expand All @@ -49,7 +50,7 @@ function runPackageManager(cwd: string, scriptName?: string) {
}

const useYarn = packageManager.packageManager === 'yarn';
const args = toPackageManagerArgs(useYarn, scriptName);
const args = toPackageManagerArgs(useYarn, buildType, scriptName);
try {
execa.sync(packageManager.executable, args, {
cwd,
Expand All @@ -65,16 +66,26 @@ function runPackageManager(cwd: string, scriptName?: string) {
}
}

function toPackageManagerArgs(useYarn: boolean, scriptName?: string) {
function toPackageManagerArgs(useYarn: boolean, buildType?: BuildType, scriptName?: string) {
if (scriptName) {
return useYarn ? [scriptName] : ['run-script', scriptName];
}
return useYarn ? [] : ['install'];

const args = useYarn ? [] : ['install'];

if (buildType === BuildType.PROD) {
args.push('--production');
}

return args;
}

function isBuildStale(resourceDir: string, lastBuildTimeStamp: Date) {
function isBuildStale(resourceDir: string, lastBuildTimeStamp: Date, buildType: BuildType, lastBuildType?: BuildType) {
const dirTime = new Date(fs.statSync(resourceDir).mtime);
if (dirTime > lastBuildTimeStamp) {
// If the last build type not matching we have to flag a stale build to force
// a npm/yarn install. This way devDependencies will not be packaged when we
// push to the cloud.
if (dirTime > lastBuildTimeStamp || buildType !== lastBuildType) {
return true;
}
const fileUpdatedAfterLastBuild = glob
Expand Down

0 comments on commit 8eb650e

Please sign in to comment.