diff --git a/Readme.md b/Readme.md index 488d864ca64..a0611ff40b9 100644 --- a/Readme.md +++ b/Readme.md @@ -34,7 +34,7 @@ The AWS Amplify CLI is a toolchain which includes a robust feature set for simpl ## Install the CLI -- Requires Node.js® version 10 or later +- Requires Node.js® version 12 or later Install and configure the Amplify CLI as follows: diff --git a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs index ea6242992e7..43bc61b7bd3 100644 --- a/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs +++ b/packages/amplify-category-auth/resources/cloudformation-templates/auth-template.yml.ejs @@ -127,7 +127,7 @@ Resources: Mutable: true <% } %> <% } %> - <%if (!props.breakCircularDependency && props.triggers !== '{}' && props.dependsOn) { %> + <%if (!props.breakCircularDependency && props.triggers && props.dependsOn) { %> LambdaConfig: <%if (props.dependsOn.find(i => i.resourceName.includes('CreateAuthChallenge'))) { %> CreateAuthChallenge: !Ref function<%=props.resourceName%>CreateAuthChallengeArn diff --git a/packages/amplify-cli/src/__tests__/init-steps/s1-initFrontend.test.ts b/packages/amplify-cli/src/__tests__/init-steps/s1-initFrontend.test.ts new file mode 100644 index 00000000000..dd1da50ec12 --- /dev/null +++ b/packages/amplify-cli/src/__tests__/init-steps/s1-initFrontend.test.ts @@ -0,0 +1,18 @@ +import { getSuitableFrontend } from '../../init-steps/s1-initFrontend'; + +describe('getSuitableFrontend', () => { + it('supports headless inputs', () => { + const context = { + exeInfo: { + inputParams: { + amplify: { + frontend: 'ios', + }, + }, + }, + } as any; + const frontendPlugins = { ios: '' } as any; + const result = getSuitableFrontend(context, frontendPlugins, ''); + expect(result).toStrictEqual('ios'); + }); +}); diff --git a/packages/amplify-cli/src/init-steps/s0-analyzeProject.ts b/packages/amplify-cli/src/init-steps/s0-analyzeProject.ts index d91fd8e5c0d..95e0ee5a2ef 100644 --- a/packages/amplify-cli/src/init-steps/s0-analyzeProject.ts +++ b/packages/amplify-cli/src/init-steps/s0-analyzeProject.ts @@ -72,7 +72,7 @@ async function displayAndSetDefaults(context: $TSContext, projectPath: string, p await displayConfigurationDefaults(context, defaultProjectName, defaultEnv, defaultEditorName); const frontendPlugins = getFrontendPlugins(context); - const defaultFrontend = getSuitableFrontend(frontendPlugins, projectPath); + const defaultFrontend = getSuitableFrontend(context, frontendPlugins, projectPath); const frontendModule = require(frontendPlugins[defaultFrontend]); await frontendModule.displayFrontendDefaults(context, projectPath); diff --git a/packages/amplify-cli/src/init-steps/s1-initFrontend.ts b/packages/amplify-cli/src/init-steps/s1-initFrontend.ts index a32130d93e5..df8710c179f 100644 --- a/packages/amplify-cli/src/init-steps/s1-initFrontend.ts +++ b/packages/amplify-cli/src/init-steps/s1-initFrontend.ts @@ -12,7 +12,7 @@ export async function initFrontend(context: $TSContext) { } const frontendPlugins = getFrontendPlugins(context); - const suitableFrontend = getSuitableFrontend(frontendPlugins, context.exeInfo.localEnvInfo.projectPath); + const suitableFrontend = getSuitableFrontend(context, frontendPlugins, context.exeInfo.localEnvInfo.projectPath); const frontend = await getFrontendHandler(context, frontendPlugins, suitableFrontend); context.exeInfo.projectConfig.frontend = frontend; @@ -22,7 +22,13 @@ export async function initFrontend(context: $TSContext) { return context; } -export function getSuitableFrontend(frontendPlugins: $TSAny, projectPath: string) { +export function getSuitableFrontend(context: $TSContext, frontendPlugins: $TSAny, projectPath: string) { + let headlessFrontend = context?.exeInfo?.inputParams?.amplify?.frontend; + + if (headlessFrontend && headlessFrontend in frontendPlugins) { + return headlessFrontend; + } + let suitableFrontend; let fitToHandleScore = -1; diff --git a/packages/amplify-cli/src/utils/post-install-initialization.ts b/packages/amplify-cli/src/utils/post-install-initialization.ts index e1d7d364e4d..f3eb0ebdab8 100644 --- a/packages/amplify-cli/src/utils/post-install-initialization.ts +++ b/packages/amplify-cli/src/utils/post-install-initialization.ts @@ -49,6 +49,7 @@ const resolvePackageRoot = (packageName: string) => { // Registry of packages that have files that need to be copied to the .amplify folder on CLI installation const copyPkgAssetRegistry = [ 'amplify-dynamodb-simulator', + 'amplify-frontend-ios', 'amplify-go-function-runtime-provider', 'amplify-java-function-runtime-provider', 'amplify-python-function-runtime-provider', diff --git a/packages/amplify-frontend-ios/index.js b/packages/amplify-frontend-ios/index.js index bf1e2bc5dba..d440dd05ad8 100644 --- a/packages/amplify-frontend-ios/index.js +++ b/packages/amplify-frontend-ios/index.js @@ -1,6 +1,6 @@ const path = require('path'); -const amplifyApp = require('amplify-app'); -const { FeatureFlags } = require('amplify-cli-core'); +const { FeatureFlags, pathManager } = require('amplify-cli-core'); +const { importConfig, importModels } = require('./lib/amplify-xcode'); const initializer = require('./lib/initializer'); const projectScanner = require('./lib/project-scanner'); const configManager = require('./lib/configuration-manager'); @@ -59,22 +59,35 @@ async function executeAmplifyCommand(context) { await commandModule.run(context); } -const postEvents = new Set(['PostInit', 'PostCodegenModels', 'PostPull']); - async function handleAmplifyEvent(context, args) { const { frontend } = context.amplify.getProjectConfig(); const isXcodeIntegrationEnabled = FeatureFlags.getBoolean('frontend-ios.enableXcodeIntegration'); const isFrontendiOS = frontend === 'ios'; - if (isFrontendiOS && isXcodeIntegrationEnabled && postEvents.has(args.event)) { - await amplifyApp.run({ - skipEnvCheck: true, - platform: frontend, - skipInit: true, - internalOnlyIosCallback: true, - }); + const isMacOs = process.platform === 'darwin'; + if (!isFrontendiOS || !isXcodeIntegrationEnabled || !isMacOs) { + return; + } + context.print.info('Updating iOS project'); + const projectPath = pathManager.findProjectRoot(); + switch (args.event) { + case 'PostInit': + await importConfig({ path: projectPath }); + break; + case 'PostCodegenModels': + await importModels({ path: projectPath }); + break; + case 'PostPull': + await importConfig({ path: projectPath }); + await importModels({ path: projectPath }); + break; + default: + break; } + context.print.info('Amplify setup completed successfully.'); } +const getPackageAssetPaths = async () => ['resources']; + module.exports = { constants, scanProject, @@ -89,4 +102,5 @@ module.exports = { executeAmplifyCommand, handleAmplifyEvent, deleteConfig: deleteAmplifyConfig, + getPackageAssetPaths, }; diff --git a/packages/amplify-frontend-ios/lib/amplify-xcode.js b/packages/amplify-frontend-ios/lib/amplify-xcode.js new file mode 100644 index 00000000000..a2b93b014d7 --- /dev/null +++ b/packages/amplify-frontend-ios/lib/amplify-xcode.js @@ -0,0 +1,71 @@ +// +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// ############################################################################ +// Auto-generated file using `build` NPM script. Do not edit this file manually +// ############################################################################ + +const execa = require('execa'); +const path = require('path'); +const { pathManager } = require('amplify-cli-core'); + +const BINARY_PATH = 'resources/amplify-xcode'; +const PACKAGE_NAME = 'amplify-frontend-ios'; +const amplifyXcodePath = () => path.join(pathManager.getAmplifyPackageLibDirPath(PACKAGE_NAME), BINARY_PATH); + +/** + * Import Amplify configuration files + * @param {Object} params + * @param {String} params.path - Project base path + */ +async function importConfig(params) { + let command = `${amplifyXcodePath()} import-config`; + if (params['path']) { + command += ` --path=${params['path']}`; + } + await execa.command(command, { stdout: 'inherit' }); +} + +/** + * Import Amplify models + * @param {Object} params + * @param {String} params.path - Project base path + */ +async function importModels(params) { + let command = `${amplifyXcodePath()} import-models`; + if (params['path']) { + command += ` --path=${params['path']}`; + } + await execa.command(command, { stdout: 'inherit' }); +} + +/** + * Generates a JSON description of the CLI and its commands + * @param {Object} params + * @param {String} params.output-path - Path to save the output of generated schema file + */ +async function generateSchema(params) { + let command = `${amplifyXcodePath()} generate-schema`; + if (params['output-path']) { + command += ` --output-path=${params['output-path']}`; + } + await execa.command(command, { stdout: 'inherit' }); +} + +module.exports = { + importConfig, + importModels, + generateSchema, +}; diff --git a/packages/amplify-frontend-ios/native-bindings-codegen/index.js b/packages/amplify-frontend-ios/native-bindings-codegen/index.js new file mode 100644 index 00000000000..fffc9d2f1e2 --- /dev/null +++ b/packages/amplify-frontend-ios/native-bindings-codegen/index.js @@ -0,0 +1,127 @@ +const path = require('path'); +const fs = require('fs'); +const _ = require('lodash'); + +const CODEGEN_TEMPLATES_FOLDER = 'templates'; + +const canonicalFunctionName = commandName => _.camelCase(commandName); + +const getTemplate = templatName => { + const templatePath = path.join(__dirname, CODEGEN_TEMPLATES_FOLDER, templatName); + const template = fs.readFileSync(templatePath).toString(); + return _.template(template, { interpolate: /<%=([\s\S]+?)%>/g }); +}; + +/** + * Generates function signature docs + * @param {String} abstract + * @param {Array<{name: String, type: String, help: String}>} parameters + */ +const generateFunctionDocs = (abstract, parameters) => { + return ` +/** + * ${abstract} + * @param {Object} params +${parameters.map(p => ` * @param {${p.type}} params.${p.name} - ${p.help}`).join('\n')} + */`; +}; + +/** + * Given a list of parameters of type `Parameter` generates + * proper CLI call signature. + * Parameter { + * kind: option | flag | argument + * name: string + * type: string + * help: string + * } + * @param {Array<{ + * name: String, + * kind:"option" | "flag" | "argument" + * type: String, + * help: String}>} parameters + * @returns {Array>} + */ +const generateCommandParameters = parameters => { + return parameters.map(param => { + const { kind, name } = param; + const funcParamValue = `params['${name}']`; + let output; + switch (kind) { + case 'option': + output = [`if (${funcParamValue}) {`, ` command += \` --${name}=\${${funcParamValue}}\`;`, ` }`]; + break; + case 'flag': + output = [`if (${funcParamValue}) {`, ` command += \` --${name}\`;`, ` }`]; + break; + case 'argument': + output = [` command += \` \${${funcParamValue}}\`;`]; + } + return output; + }); +}; + +/** + * Given a command schema generates data used in + * template function declaration + * @param {Object} commandSchema + * @param {String} commandSchema.abstract + * @param {String} commandSchema.name + * @param {Array} commandSchema.parameters + */ +const generateFunctionBodyData = commandSchema => { + const { abstract, name, parameters } = commandSchema; + return { + __FUNCTION_DOCS__: generateFunctionDocs(abstract, parameters), + __FUNCTION_NAME__: canonicalFunctionName(name), + __COMMAND_NAME__: name, + __COMMAND_PARAMS__: generateCommandParameters(parameters) + .map(p => ` ${p.join('\n')}`) + .join('\n'), + }; +}; + +/** + * Given an `amplify-xcode` schema, generates the commonjs + * exported module declaration + * @param {Object} schema amplify-xcode schema @see amplify-xcode.json + */ +const generateModuleExports = schema => { + let output = ['module.exports = {']; + schema.commands.forEach(command => { + output.push(` ${canonicalFunctionName(command.name)},`); + }); + output.push('};'); + + return output; +}; + +/** + * Given an `amplify-xcode` schema, generates JS + * bindings to safely call `amplify-xcode`. + * @param {Object} schema amplify-xcode schema + * @param {String} outputPath generated JS bindings file path + */ +const generateNativeBindings = (schema, outputPath) => { + const preamble = getTemplate('preamble.jst')(); + const functionTemplate = getTemplate('function.jst'); + // generate functions body + let output = preamble; + output += schema.commands + .map(command => { + return functionTemplate(generateFunctionBodyData(command)); + }) + .join(''); + + // generate exports + output += '\n' + generateModuleExports(schema).join('\n') + '\n'; + + fs.writeFileSync(outputPath, output); +}; + +module.exports = { + generateCommandParameters, + generateFunctionBodyData, + generateModuleExports, + generateNativeBindings, +}; diff --git a/packages/amplify-frontend-ios/native-bindings-codegen/templates/function.jst b/packages/amplify-frontend-ios/native-bindings-codegen/templates/function.jst new file mode 100644 index 00000000000..f547dc4548a --- /dev/null +++ b/packages/amplify-frontend-ios/native-bindings-codegen/templates/function.jst @@ -0,0 +1,6 @@ +<%= __FUNCTION_DOCS__ %> +async function <%= __FUNCTION_NAME__ %>(params) { + let command = `${amplifyXcodePath()} <%= __COMMAND_NAME__ %>`; +<%=__COMMAND_PARAMS__ %> + await execa.command(command, { stdout: 'inherit' }); +} diff --git a/packages/amplify-frontend-ios/native-bindings-codegen/templates/preamble.jst b/packages/amplify-frontend-ios/native-bindings-codegen/templates/preamble.jst new file mode 100644 index 00000000000..e048867c6fa --- /dev/null +++ b/packages/amplify-frontend-ios/native-bindings-codegen/templates/preamble.jst @@ -0,0 +1,26 @@ +// +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// ############################################################################ +// Auto-generated file using `build` NPM script. Do not edit this file manually +// ############################################################################ + +const execa = require('execa'); +const path = require('path'); +const { pathManager } = require('amplify-cli-core'); + +const BINARY_PATH = 'resources/amplify-xcode'; +const PACKAGE_NAME = 'amplify-frontend-ios'; +const amplifyXcodePath = () => path.join(pathManager.getAmplifyPackageLibDirPath(PACKAGE_NAME), BINARY_PATH); diff --git a/packages/amplify-frontend-ios/package.json b/packages/amplify-frontend-ios/package.json index c4cc541b417..daf5d4a2494 100644 --- a/packages/amplify-frontend-ios/package.json +++ b/packages/amplify-frontend-ios/package.json @@ -16,13 +16,16 @@ "aws" ], "scripts": { + "generate-xcode-bindings": "node ./scripts/native-bindings-gen", "test": "jest", "test-watch": "jest --watch" }, "dependencies": { - "amplify-app": "2.23.5", + "amplify-cli-core": "1.21.2", + "execa": "^4.1.0", "fs-extra": "^8.1.0", - "graphql-config": "^2.2.1" + "graphql-config": "^2.2.1", + "lodash": "^4.17.21" }, "jest": { "collectCoverage": true, diff --git a/packages/amplify-frontend-ios/resources/amplify-xcode b/packages/amplify-frontend-ios/resources/amplify-xcode new file mode 100755 index 00000000000..3209bd7b34d Binary files /dev/null and b/packages/amplify-frontend-ios/resources/amplify-xcode differ diff --git a/packages/amplify-frontend-ios/resources/amplify-xcode.json b/packages/amplify-frontend-ios/resources/amplify-xcode.json new file mode 100644 index 00000000000..2d22a55fd44 --- /dev/null +++ b/packages/amplify-frontend-ios/resources/amplify-xcode.json @@ -0,0 +1,41 @@ +{ + "commands": [ + { + "name": "import-config", + "abstract": "Import Amplify configuration files", + "parameters": [ + { + "kind": "option", + "name": "path", + "type": "String", + "help": "Project base path" + } + ] + }, + { + "name": "import-models", + "abstract": "Import Amplify models", + "parameters": [ + { + "kind": "option", + "name": "path", + "type": "String", + "help": "Project base path" + } + ] + }, + { + "name": "generate-schema", + "abstract": "Generates a JSON description of the CLI and its commands", + "parameters": [ + { + "kind": "option", + "name": "output-path", + "type": "String", + "help": "Path to save the output of generated schema file" + } + ] + } + ], + "abstract": "Auto generated JSON representation of amplify-xcode CLI" +} diff --git a/packages/amplify-frontend-ios/scripts/native-bindings-gen.js b/packages/amplify-frontend-ios/scripts/native-bindings-gen.js new file mode 100644 index 00000000000..ab321026714 --- /dev/null +++ b/packages/amplify-frontend-ios/scripts/native-bindings-gen.js @@ -0,0 +1,12 @@ +const path = require('path'); +const fs = require('fs'); +const { generateNativeBindings } = require('../native-bindings-codegen'); + +const OUPUT_DIR = path.join('..', 'lib'); +const SCHEMA_RELATIVE_PATH = path.join('..', 'resources', 'amplify-xcode.json'); + +const schemaPath = path.join(__dirname, SCHEMA_RELATIVE_PATH); +const schema = JSON.parse(fs.readFileSync(schemaPath)); +const outputPath = path.join(__dirname, OUPUT_DIR, 'amplify-xcode.js'); + +generateNativeBindings(schema, outputPath); diff --git a/packages/amplify-frontend-ios/tests/native-bindings-codegen.test.js b/packages/amplify-frontend-ios/tests/native-bindings-codegen.test.js new file mode 100644 index 00000000000..f5221543707 --- /dev/null +++ b/packages/amplify-frontend-ios/tests/native-bindings-codegen.test.js @@ -0,0 +1,74 @@ +const { generateCommandParameters, generateFunctionBodyData, generateModuleExports } = require('../native-bindings-codegen'); + +describe('amplify-xcode native bindings codegen', () => { + describe('should generate command parameters list', () => { + it('option', () => { + const param = { + name: 'option-name', + kind: 'option', + }; + const expected = [[`if (params['${param.name}']) {`, ` command += \` --${param.name}=\${params['${param.name}']}\`;`, ` }`]]; + expect(generateCommandParameters([param])).toEqual(expected); + }); + + it('flag', () => { + const param = { + name: 'flagName', + kind: 'flag', + }; + const expected = [[`if (params['${param.name}']) {`, ` command += \` --${param.name}\`;`, ` }`]]; + expect(generateCommandParameters([param])).toEqual(expected); + }); + + it('argument', () => { + const param = { + name: 'argName', + kind: 'argument', + }; + const expected = [[" command += ` ${params['argName']}`;"]]; + expect(generateCommandParameters([param])).toEqual(expected); + }); + }); + + describe('Functions bindings', () => { + it('should generate functions template data', () => { + const commandParam = { + name: 'option-name', + kind: 'option', + type: 'String', + help: 'Option description', + }; + const commandSchema = { + name: 'command-name', + abstract: 'Command abstract', + parameters: [commandParam], + }; + const expectedDocs = ` +/** + * ${commandSchema.abstract} + * @param {Object} params + * @param {String} params.${commandParam.name} - ${commandParam.help} + */`; + const result = generateFunctionBodyData(commandSchema); + expect(result.__FUNCTION_NAME__).toEqual('commandName'); + expect(result.__FUNCTION_DOCS__).toEqual(expectedDocs); + expect(result.__COMMAND_NAME__).toEqual(commandSchema.name); + }); + + it('should generate commonjs module exports', () => { + const command1 = { + name: 'command-name', + abstract: 'Command abstract', + }; + const command2 = { + name: 'another-command-name', + abstract: 'Another command abstract', + }; + const schema = { + commands: [command1, command2], + }; + const result = generateModuleExports(schema); + expect(result).toEqual(['module.exports = {', ' commandName,', ' anotherCommandName,', '};']); + }); + }); +}); diff --git a/packages/amplify-python-function-runtime-provider/package.json b/packages/amplify-python-function-runtime-provider/package.json index 647de320071..d9ff66f6150 100644 --- a/packages/amplify-python-function-runtime-provider/package.json +++ b/packages/amplify-python-function-runtime-provider/package.json @@ -26,6 +26,7 @@ "archiver": "^3.1.1", "execa": "^4.1.0", "glob": "^7.1.6", + "ini": "^1.3.5", "semver": "^7.1.3", "which": "^2.0.2" }, diff --git a/packages/amplify-python-function-runtime-provider/src/util/pyUtils.ts b/packages/amplify-python-function-runtime-provider/src/util/pyUtils.ts index 1b9ce1f79b5..961b40fb12a 100644 --- a/packages/amplify-python-function-runtime-provider/src/util/pyUtils.ts +++ b/packages/amplify-python-function-runtime-provider/src/util/pyUtils.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import { ExecOptions } from 'child_process'; import execa from 'execa'; import * as which from 'which'; +import { parse } from 'ini'; // Gets the pipenv dir where this function's dependencies are located export async function getPipenvDir(srcRoot: string): Promise { @@ -13,8 +14,7 @@ export async function getPipenvDir(srcRoot: string): Promise { throw new Error(`Could not find 'python3' or 'python' executable in the PATH.`); } - const pyVersion = await execAsStringPromise(`${pyBinary} --version`); - let pipEnvPath = path.join(pipEnvDir, 'lib', 'python' + majMinPyVersion(pyVersion), 'site-packages'); + let pipEnvPath = path.join(pipEnvDir, 'lib', `python${getPipfilePyVersion(path.join(srcRoot, 'Pipfile'))}`, 'site-packages'); if (process.platform.startsWith('win')) { pipEnvPath = path.join(pipEnvDir, 'Lib', 'site-packages'); } @@ -29,10 +29,7 @@ export function majMinPyVersion(pyVersion: string): string { throw new Error(`Cannot interpret Python version "${pyVersion}"`); } const versionNum = pyVersion.split(' ')[1]; - return versionNum - .split('.') - .slice(0, 2) - .join('.'); + return versionNum.split('.').slice(0, 2).join('.'); } // wrapper for executing a shell command and returning the result as a string promise @@ -65,3 +62,12 @@ export const getPythonBinaryName = (): string | undefined => { } } }; + +const getPipfilePyVersion = (pipfilePath: string) => { + const pipfile = parse(fs.readFileSync(pipfilePath, 'utf-8')); + const version = pipfile?.requires?.python_version; + if (!version) { + throw new Error(`Did not find Python version specified in ${pipfilePath}`); + } + return version as string; +};