Skip to content

Commit

Permalink
Merge pull request #37 from HubSpot/br-top-level-exports-2
Browse files Browse the repository at this point in the history
Port functions file to lib
  • Loading branch information
brandenrodgers authored Sep 27, 2023
2 parents 15e7026 + 713849f commit ea64393
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 2 deletions.
19 changes: 19 additions & 0 deletions lang/en.lyaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ en:
missingEnv: "Unable to load config from environment variables: Missing env"
unknownAuthType: "Unable to load config from environment variables: Unknown auth type"
cms:
functions:
updateExistingConfig:
unableToReadFile: "The file {{ configFilePath }} could not be read"
invalidJSON: "The file {{ configFilePath }} is not valid JSON"
couldNotUpdateFile: "The file {{ configFilePath }} could not be updated"
createFunction:
destPathAlreadyExists: "The {{ path }} path already exists"
createdDest: "Created {{ path }}"
failedToCreateFile: "The file {{ configFilePath }} could not be created"
createdFunctionFile: "Created {{ path }}"
createdConfigFile: "Created {{ path }}"
success: "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out"
handleFieldsJs:
convertFieldsJs:
creating: "Creating child process with pid {{ pid }}"
Expand Down Expand Up @@ -204,6 +216,13 @@ en:
saveOutput: "There was an error saving the json output of {{ path }}"
createTmpDirSync: "An error occured writing temporary project source."
cleanupTmpDirSync: "There was an error deleting the temporary project source"
functions:
updateExistingConfig:
configIsNotObjectError: "The existing {{ configFilePath }} is not an object"
endpointAreadyExistsError: "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}"
createFunction:
nestedConfigError: "Cannot create a functions directory inside '{{ ancestorConfigPath }}'"
jsFileConflictError: "The JavaScript file at '{{ functionFilePath }}'' already exists"
sandboxes:
createSandbox: "There was an error creating your sandbox."
deleteSandbox: "There was an error deleting your sandbox."
Expand Down
249 changes: 249 additions & 0 deletions lib/cms/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import fs from 'fs-extra';
import path from 'path';
import findup from 'findup-sync';
import { getCwd } from '../path';
import { downloadGithubRepoContents } from '../github';
import { debug, makeTypedLogger } from '../../utils/logger';
import { isObject } from '../../utils/objectUtils';
import { throwErrorWithMessage } from '../../errors/standardErrors';
import { throwFileSystemError } from '../../errors/fileSystemErrors';
import { BaseError } from '../../types/Error';
import { LogCallbacksArg } from '../../types/LogCallbacks';

const i18nKey = 'cms.functions';

type Config = {
runtime: string;
version: string;
environment: object;
secrets: Array<string>;
endpoints: {
[key: string]: {
method: string;
file: string;
};
};
};

function createEndpoint(
endpointMethod: string,
filename: string
): { method: string; file: string } {
return {
method: endpointMethod || 'GET',
file: filename,
};
}

type ConfigInfo = {
endpointPath: string;
endpointMethod: string;
functionFile: string;
};

function createConfig({
endpointPath,
endpointMethod,
functionFile,
}: ConfigInfo): Config {
return {
runtime: 'nodejs18.x',
version: '1.0',
environment: {},
secrets: [],
endpoints: {
[endpointPath]: createEndpoint(endpointMethod, functionFile),
},
};
}

function writeConfig(configFilePath: string, config: Config): void {
const configJson = JSON.stringify(config, null, ' ');
fs.writeFileSync(configFilePath, configJson);
}

function updateExistingConfig(
configFilePath: string,
{ endpointPath, endpointMethod, functionFile }: ConfigInfo
): void {
let configString!: string;
try {
configString = fs.readFileSync(configFilePath).toString();
} catch (err) {
debug(`${i18nKey}.updateExistingConfig.unableToReadFile`, {
configFilePath,
});
throwFileSystemError(err as BaseError, {
filepath: configFilePath,
read: true,
});
}

let config!: Config;
try {
config = JSON.parse(configString) as Config;
} catch (err) {
debug(`${i18nKey}.updateExistingConfig.invalidJSON`, {
configFilePath,
});
throwFileSystemError(err as BaseError, {
filepath: configFilePath,
read: true,
});
}

if (!isObject(config)) {
throwErrorWithMessage(
`${i18nKey}.updateExistingConfig.configIsNotObjectError`,
{ configFilePath }
);
}
if (config.endpoints) {
if (config.endpoints[endpointPath]) {
throwErrorWithMessage(
`${i18nKey}.updateExistingConfig.endpointAreadyExistsError`,
{
configFilePath,
endpointPath,
}
);
} else {
config.endpoints[endpointPath] = createEndpoint(
endpointMethod,
functionFile
);
}
} else {
config.endpoints = {
[endpointPath]: createEndpoint(endpointMethod, functionFile),
};
}
try {
writeConfig(configFilePath, config);
} catch (err) {
debug(`${i18nKey}.updateExistingConfig.couldNotUpdateFile`, {
configFilePath,
});
throwFileSystemError(err as BaseError, {
filepath: configFilePath,
write: true,
});
}
}

type FunctionInfo = {
functionsFolder: string;
filename: string;
endpointPath: string;
endpointMethod: string;
};

type FunctionOptions = {
allowExistingFile?: boolean;
};

const createFunctionCallbackKeys = [
'destPathAlreadyExists',
'createdDest',
'createdFunctionFile',
'createdConfigFile',
'success',
];

export async function createFunction(
functionInfo: FunctionInfo,
dest: string,
options: FunctionOptions,
logCallbacks?: LogCallbacksArg<typeof createFunctionCallbackKeys>
): Promise<void> {
const logger = makeTypedLogger<typeof createFunctionCallbackKeys>(
logCallbacks,
`${i18nKey}.createFunction`
);
const { functionsFolder, filename, endpointPath, endpointMethod } =
functionInfo;

const allowExistingFile = options.allowExistingFile || false;

const ancestorFunctionsConfig = findup('serverless.json', {
cwd: getCwd(),
nocase: true,
});

if (ancestorFunctionsConfig) {
throwErrorWithMessage(`${i18nKey}.createFunction.nestedConfigError`, {
ancestorConfigPath: path.dirname(ancestorFunctionsConfig),
});
}

const folderName = functionsFolder.endsWith('.functions')
? functionsFolder
: `${functionsFolder}.functions`;
const functionFile = filename.endsWith('.js') ? filename : `${filename}.js`;

const destPath = path.join(dest, folderName);
if (fs.existsSync(destPath)) {
logger('destPathAlreadyExists', {
path: destPath,
});
} else {
fs.mkdirp(destPath);
logger('createdDest', {
path: destPath,
});
}
const functionFilePath = path.join(destPath, functionFile);
const configFilePath = path.join(destPath, 'serverless.json');

if (!allowExistingFile && fs.existsSync(functionFilePath)) {
throwErrorWithMessage(`${i18nKey}.createFunction.jsFileConflictError`, {
functionFilePath,
});
}

await downloadGithubRepoContents(
'HubSpot/cms-sample-assets',
'functions/sample-function.js',
functionFilePath
);

logger('createdFunctionFile', {
path: functionFilePath,
});

if (fs.existsSync(configFilePath)) {
updateExistingConfig(configFilePath, {
endpointPath,
endpointMethod,
functionFile,
});

logger('createdFunctionFile', {
path: functionFilePath,
});
logger('success', {
endpointPath: endpointPath,
folderName,
});
} else {
const config = createConfig({ endpointPath, endpointMethod, functionFile });
try {
writeConfig(configFilePath, config);
} catch (err) {
debug(`${i18nKey}.createFunction.failedToCreateFile`, {
configFilePath,
});
throwFileSystemError(err as BaseError, {
filepath: configFilePath,
write: true,
});
}
logger('createdConfigFile', {
path: configFilePath,
});
logger('success', {
endpointPath: endpointPath,
folderName,
});
}
}
8 changes: 7 additions & 1 deletion lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,13 @@ export async function downloadGithubRepoContents(
return fetchGitHubRepoContentFromDownloadUrl(downloadPath, download_url);
};

const contentPromises = contentsResp.map(downloadContent);
let contentPromises;

if (Array.isArray(contentsResp)) {
contentPromises = contentsResp.map(downloadContent);
} else {
contentPromises = [downloadContent(contentsResp)];
}

Promise.all(contentPromises);
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"access": "public"
},
"scripts": {
"build": "tsc --rootDir . --outdir dist && yarn copy-files",
"build": "rm -rf ./dist/ && tsc --rootDir . --outdir dist && yarn copy-files",
"check-main": "branch=$(git rev-parse --abbrev-ref HEAD) && [ $branch = main ] || (echo 'Error: New release can only be published on main branch' && exit 1)",
"copy-files": "cp -r lang dist/lang",
"lint": "eslint --max-warnings=0 . && prettier --check ./**/*.ts",
Expand Down
4 changes: 4 additions & 0 deletions utils/objectUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function isObject(value: object): boolean {
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
}

0 comments on commit ea64393

Please sign in to comment.