diff --git a/e2e/nx-flutter-e2e/tests/nx-flutter.spec.ts b/e2e/nx-flutter-e2e/tests/nx-flutter.test.ts similarity index 84% rename from e2e/nx-flutter-e2e/tests/nx-flutter.spec.ts rename to e2e/nx-flutter-e2e/tests/nx-flutter.test.ts index f220b90a..5f58eb53 100644 --- a/e2e/nx-flutter-e2e/tests/nx-flutter.spec.ts +++ b/e2e/nx-flutter-e2e/tests/nx-flutter.test.ts @@ -5,11 +5,11 @@ import { runNxCommandAsync, uniq, } from '@nrwl/nx-plugin/testing'; -import * as inquirer from 'inquirer'; jest.mock('inquirer'); // we mock 'inquirer' to bypass the interactive prompt +import * as inquirer from 'inquirer'; -describe('nx-flutter e2e', () => { +xdescribe('nx-flutter e2e', () => { beforeEach(() => { jest.spyOn(inquirer, 'prompt').mockResolvedValue({ @@ -19,20 +19,18 @@ describe('nx-flutter e2e', () => { }); }); - afterEach(() => - (inquirer.prompt as jest.MockedFunction< - typeof inquirer.prompt - >).mockRestore() - ); + afterEach(() => { + jest.resetAllMocks(); + }); it('should create nx-flutter project with default options', async (done) => { const appName = uniq('nx-flutter'); ensureNxProject('@nxrocks/nx-flutter', 'dist/packages/nx-flutter'); await runNxCommandAsync(`generate @nxrocks/nx-flutter:create ${appName} --interactive=false`); - const builders = [ + const executors = [ { name: 'analyze', output: `Analyzing ${appName}` }, - + //{ name: 'assemble', output: `Assembling ${appName}` }, //{ name: 'attach', output: `Attaching ${appName}` }, @@ -64,16 +62,16 @@ describe('nx-flutter e2e', () => { { name: 'test', output: `All tests passed!` }, ]; - for(const builder of builders){ - const result = await runNxCommandAsync(`run ${appName}:${builder.name}`); - expect(result.stdout).toContain(builder.output); + for (const executor of executors) { + const result = await runNxCommandAsync(`run ${appName}:${executor.name}`); + expect(result.stdout).toContain(executor.output); } expect(() => checkFilesExist(`apps/${appName}/pubspec.yaml`) ).not.toThrow(); done(); - }, 300000); + }, 180000); it('should create nx-flutter project with given options', async (done) => { const appName = uniq('nx-flutter'); @@ -89,29 +87,29 @@ describe('nx-flutter e2e', () => { ensureNxProject('@nxrocks/nx-flutter', 'dist/packages/nx-flutter'); await runNxCommandAsync(`generate @nxrocks/nx-flutter:create ${appName} --interactive=false --org=${org} --description="${description}" --androidLanguage=${androidLanguage} --iosLanguage=${iosLanguage} --template=${template} --platforms="${platforms}" --pub=${pub} --offline=${offline} `); - const builders = [ + const executors = [ { name: 'clean', output: `Deleting flutter_export_environment.sh...` }, { name: 'format', output: `Done in ` }, { name: 'test', output: `All tests passed!` }, ]; - for(const builder of builders){ - const result = await runNxCommandAsync(`run ${appName}:${builder.name}`); - expect(result.stdout).toContain(builder.output); + for (const executor of executors) { + const result = await runNxCommandAsync(`run ${appName}:${executor.name}`); + expect(result.stdout).toContain(executor.output); } - + expect(() => checkFilesExist(`apps/${appName}/pubspec.yaml`, - `apps/${appName}/android/build.gradle`, - `apps/${appName}/ios/Runner.xcodeproj`, - `apps/${appName}/android/app/src/main/java/com/tinesoft/${appName.replace('-','_')}/MainActivity.java` - ) + `apps/${appName}/android/build.gradle`, + `apps/${appName}/ios/Runner.xcodeproj`, + `apps/${appName}/android/app/src/main/java/com/tinesoft/${appName.replace('-', '_')}/MainActivity.java` + ) ).not.toThrow(); done(); - }, 600000); + }, 180000); - xdescribe('--directory', () => { + describe('--directory', () => { it('should create src in the specified directory', async (done) => { const appName = uniq('nx-flutter'); ensureNxProject('@nxrocks/nx-flutter', 'dist/packages/nx-flutter'); diff --git a/e2e/nx-flutter-e2e/tsconfig.spec.json b/e2e/nx-flutter-e2e/tsconfig.spec.json index 29efa430..af4ac638 100644 --- a/e2e/nx-flutter-e2e/tsconfig.spec.json +++ b/e2e/nx-flutter-e2e/tsconfig.spec.json @@ -5,5 +5,12 @@ "module": "commonjs", "types": ["jest", "node"] }, - "include": ["**/*.spec.ts", "**/*.d.ts"] + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] } diff --git a/packages/nx-flutter/builders.json b/packages/nx-flutter/builders.json deleted file mode 100644 index ac4f33be..00000000 --- a/packages/nx-flutter/builders.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json", - "builders": { - } -} diff --git a/packages/nx-flutter/collection.json b/packages/nx-flutter/collection.json deleted file mode 100644 index 5b4056c9..00000000 --- a/packages/nx-flutter/collection.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", - "name": "nx-flutter", - "version": "0.0.1", - "schematics": { - "application": { - "factory": "./src/schematics/application/schematic", - "schema": "./src/schematics/application/schema.json", - "description": "Schematic to generate the application", - "aliases": ["app", "create"] - } - } -} diff --git a/packages/nx-flutter/executors.json b/packages/nx-flutter/executors.json new file mode 100644 index 00000000..461735fd --- /dev/null +++ b/packages/nx-flutter/executors.json @@ -0,0 +1,4 @@ +{ + "executors": { + } +} diff --git a/packages/nx-flutter/generators.json b/packages/nx-flutter/generators.json new file mode 100644 index 00000000..3024e7f0 --- /dev/null +++ b/packages/nx-flutter/generators.json @@ -0,0 +1,12 @@ +{ + "name": "nx-flutter", + "version": "0.0.1", + "generators": { + "application": { + "factory": "./src/generators/application/generator", + "schema": "./src/generators/application/schema.json", + "description": "Generator to generate the application", + "aliases": ["app", "create"] + } + } +} diff --git a/packages/nx-flutter/package.json b/packages/nx-flutter/package.json index 119a4a33..d6b7e85f 100644 --- a/packages/nx-flutter/package.json +++ b/packages/nx-flutter/package.json @@ -7,8 +7,8 @@ "access": "public" }, "main": "src/index.js", - "schematics": "./collection.json", - "builders": "./builders.json", + "generators": "./generators.json", + "executors": "./executors.json", "license": "MIT", "author": "Tine Kondo ", "repository": { @@ -28,6 +28,8 @@ "dart" ], "dependencies": { + "@nrwl/workspace": "*", + "@nrwl/devkit": "*", "inquirer": "^7.3.3" } } diff --git a/packages/nx-flutter/src/generators/application/generator.spec.ts b/packages/nx-flutter/src/generators/application/generator.spec.ts new file mode 100644 index 00000000..f4b0e843 --- /dev/null +++ b/packages/nx-flutter/src/generators/application/generator.spec.ts @@ -0,0 +1,171 @@ +import { Tree, logger, readProjectConfiguration } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import each from 'jest-each'; + +import { applicationGenerator } from './generator'; +import { ApplicationGeneratorOptions } from './schema'; + +jest.mock('child_process'); // we need to mock 'execSync' so that it doesn't really run 'flutter' (reserved to e2e testing) (see __mocks__/child_process.js) + +jest.mock('inquirer'); // we mock 'inquirer' to bypass the interactive prompt +import * as inquirer from 'inquirer'; + + +const appCommands = [ + { key: 'assemble', value: 'assemble' }, + { key: 'attach', value: 'attach' }, + { key: 'drive', value: 'drive' }, + { key: 'genL10n', value: 'gen-l10n' }, + { key: 'install', value: 'install' }, + { key: 'run', value: 'run' }, +]; + +const pluginOrModOnlyCommands = [ + { key: 'buildAar', value: 'build aar' }, +]; + +const androidOnlyCommands = [ + { key: 'buildAar', value: 'build aar' }, + { key: 'buildApk', value: 'build apk' }, + { key: 'buildAppbundle', value: 'build appbundle' }, + { key: 'buildBundle', value: 'build bundle' }, +]; + +const iOsOnlyCommands = [ + { key: 'buildIos', value: 'build ios' }, + { key: 'buildIosFramework', value: 'build ios-framework' }, + { key: 'buildIpa', value: 'build ipa' }, +]; + +describe('application generator', () => { + let tree: Tree; + const options: ApplicationGeneratorOptions = { + name: 'testapp', + template: 'app', + platforms: ['android', 'ios', 'web', 'linux', 'windows', 'macos'], + interactive: true + }; + + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + + jest.spyOn(inquirer, 'prompt').mockResolvedValue({ + platforms: options.platforms, + androidLanguage: 'kotlin', + iosLanguage: 'swift' + }); + jest.spyOn(logger, 'info'); + }); + + afterEach(() => { + jest.resetAllMocks(); + } + ); + + it('should update workspace.json', async () => { + + await applicationGenerator(tree, options); + const project = readProjectConfiguration(tree, options.name); + expect(project.root).toBe(`apps/${options.name}`); + + const commonCommands = [ + { key: 'analyze', value: 'analyze' }, + { key: 'clean', value: 'clean' }, + { key: 'format', value: `format ${project.root}/*` }, + { key: 'test', value: 'test' }, + ]; + + const commands = [...commonCommands, ...appCommands, ...pluginOrModOnlyCommands, ...androidOnlyCommands, ...iOsOnlyCommands]; + commands.forEach(e => { + expect(project.targets[e.key].executor).toBe('@nrwl/workspace:run-commands'); + expect(project.targets[e.key].options.command).toBe(`flutter ${e.value}`); + }); + }); + + each` + template | shouldPromptTempate + ${'app'} | ${true} + ${'plugin'} | ${true} + ${'package'}| ${false} + ${'module'} | ${false} + `.it('should prompt user to select "platforms" when generating "$template": $shouldPromptTempate', async ({ template, shouldPromptTempate }) => { + + await applicationGenerator(tree, { ...options, template: template }); + + expect(inquirer.prompt).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + when: shouldPromptTempate, + name: 'platforms', + type: 'checkbox', + choices: expect.arrayContaining([ + { + value: "android", + name: "Android platform", + checked: true, + }, + { + value: "ios", + name: "iOS platform", + checked: true, + }, + { + value: "linux", + name: "Linux platform", + checked: true, + }, + { + value: "windows", + name: "Windows platform", + checked: true, + }, + { + value: "macos", + name: "MacOS platform", + checked: true, + }, + { + value: "web", + name: "Web platform", + checked: true, + } + ]), + }), + expect.objectContaining({ + name: 'androidLanguage', + type: 'list', + default: 'kotlin', + choices: expect.arrayContaining([ + { + value: "java", + name: "Java" + }, + { + value: "kotlin", + name: "Kotlin" + } + ]), + message: "Which Android language would you like to use?", + }), + expect.objectContaining({ + name: 'iosLanguage', + type: 'list', + default: 'swift', + choices: expect.arrayContaining([ + { + value: "objc", + name: "Objective-C" + }, + { + value: "swift", + name: "Swift" + } + ]), + message: "Which iOS language would you like to use?", + }) + ]) + ); + }); +}); diff --git a/packages/nx-flutter/src/generators/application/generator.ts b/packages/nx-flutter/src/generators/application/generator.ts new file mode 100644 index 00000000..407223dc --- /dev/null +++ b/packages/nx-flutter/src/generators/application/generator.ts @@ -0,0 +1,79 @@ +import { Tree, addProjectConfiguration, } from '@nrwl/devkit'; + +import { isFlutterInstalled } from '../../utils/flutter-utils'; +import { normalizeOptions, promptAdditionalOptions, generateFlutterProject } from './lib'; +import { ApplicationGeneratorOptions } from './schema'; + +/** + * Depending on your needs, you can change this to either `Library` or `Application` + */ +const projectType = 'application'; + +export async function applicationGenerator(tree:Tree, options: ApplicationGeneratorOptions) { + + if (!isFlutterInstalled()) { + throw new Error("'flutter' was not found on your system's PATH.\nPlease make sure you have installed it correctly.\nšŸ‘‰šŸ¾ https://flutter.dev/docs/get-started/install"); + } + + const normalizedOptions = normalizeOptions(tree,options); + + if(options.interactive ) + await promptAdditionalOptions(tree,normalizedOptions); + + const targets = {}; + const commands = [ + { key: 'analyze', value: 'analyze' }, + { key: 'clean', value: 'clean' }, + { key: 'format', value: `format ${normalizedOptions.projectRoot}/*` }, + { key: 'test', value: 'test' }, + ]; + + if(normalizedOptions.template === 'app'){ + commands.push( + { key: 'assemble', value: 'assemble' }, + { key: 'attach', value: 'attach' }, + { key: 'drive', value: 'drive' }, + { key: 'genL10n', value: 'gen-l10n' }, + { key: 'install', value: 'install' }, + { key: 'run', value: 'run' }, + ) + } + + if(normalizedOptions.platforms?.indexOf('android') != -1) { + commands.push( + {key: 'buildAar', value: 'build aar'}, + {key: 'buildApk', value: 'build apk'}, + {key: 'buildAppbundle', value: 'build appbundle'}, + {key: 'buildBundle', value: 'build bundle'}, + ) + } + + if(normalizedOptions.platforms?.indexOf('ios') != -1) { + commands.push( + {key: 'buildIos', value: 'build ios'}, + {key: 'buildIosFramework', value: 'build ios-framework'}, + {key: 'buildIpa', value: 'build ipa'}, + ) + } + + for (const command of commands) { + targets[command.key] = { + executor: `@nrwl/workspace:run-commands`, + options: { + command: `flutter ${command.value}`, + cwd: normalizedOptions.projectRoot + } + }; + } + addProjectConfiguration(tree, normalizedOptions.projectName, { + root: normalizedOptions.projectRoot, + sourceRoot: `${normalizedOptions.projectRoot}/src`, + projectType: projectType, + targets: targets, + tags: normalizedOptions.parsedTags, + }); + await generateFlutterProject(tree,normalizedOptions) + +} + +export default applicationGenerator; \ No newline at end of file diff --git a/packages/nx-flutter/src/generators/application/lib/generate-project.ts b/packages/nx-flutter/src/generators/application/lib/generate-project.ts new file mode 100644 index 00000000..e8f7ff20 --- /dev/null +++ b/packages/nx-flutter/src/generators/application/lib/generate-project.ts @@ -0,0 +1,24 @@ +import { Tree, logger } from '@nrwl/devkit'; +import { execSync } from 'child_process' +import { buildFlutterCreateOptions } from '../../../utils/flutter-utils'; + +import { NormalizedSchema } from '../schema'; + + +export async function generateFlutterProject(tree: Tree, options: NormalizedSchema): Promise { + const opts = buildFlutterCreateOptions(options); + + logger.info(`Generating Flutter project with following options : ${opts}...`); + + // Create the command to execute + const execute = `flutter create ${opts} ${options.projectRoot}`; + try { + logger.info(`Executing command: ${execute}`); + execSync(execute, { stdio: [0, 1, 2] }); + return; + } catch (e) { + logger.error(`Failed to execute command: ${execute}`); + logger.error(e); + return; + } +} diff --git a/packages/nx-flutter/src/generators/application/lib/index.ts b/packages/nx-flutter/src/generators/application/lib/index.ts new file mode 100644 index 00000000..7766f5e6 --- /dev/null +++ b/packages/nx-flutter/src/generators/application/lib/index.ts @@ -0,0 +1,3 @@ +export { generateFlutterProject } from './generate-project'; +export { normalizeOptions } from './normalize-options'; +export { promptAdditionalOptions } from './prompt-options'; \ No newline at end of file diff --git a/packages/nx-flutter/src/generators/application/lib/normalize-options.ts b/packages/nx-flutter/src/generators/application/lib/normalize-options.ts new file mode 100644 index 00000000..453f241c --- /dev/null +++ b/packages/nx-flutter/src/generators/application/lib/normalize-options.ts @@ -0,0 +1,35 @@ +import { + Tree, + names, + getWorkspaceLayout, +} from '@nrwl/devkit'; +import { ApplicationGeneratorOptions, NormalizedSchema } from '../schema'; + +/** + * Depending on your needs, you can change this to either `Library` or `Application` + */ +const projectType = 'application'; + +export function normalizeOptions(tree: Tree, + options: ApplicationGeneratorOptions +): NormalizedSchema { + const { appsDir, libsDir } = getWorkspaceLayout(tree); + const projectRootDir = projectType === 'application' ? appsDir : libsDir; + const name = names(options.name).fileName; + const projectDirectory = options.directory + ? `${names(options.directory).fileName}/${name}` + : name; + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const projectRoot = `${projectRootDir}/${projectDirectory}`; + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + return { + ...options, + projectName, + projectRoot, + projectDirectory, + parsedTags, + }; +} diff --git a/packages/nx-flutter/src/schematics/application/lib/prompt-options.ts b/packages/nx-flutter/src/generators/application/lib/prompt-options.ts similarity index 86% rename from packages/nx-flutter/src/schematics/application/lib/prompt-options.ts rename to packages/nx-flutter/src/generators/application/lib/prompt-options.ts index ee11b8a5..f60f4017 100644 --- a/packages/nx-flutter/src/schematics/application/lib/prompt-options.ts +++ b/packages/nx-flutter/src/generators/application/lib/prompt-options.ts @@ -1,6 +1,4 @@ -import { - Rule -} from '@angular-devkit/schematics'; +import { Tree } from '@nrwl/devkit'; import * as inquirer from 'inquirer'; import { AndroidLanguageType, IosLanguageType, NormalizedSchema, PlatformType } from '../schema'; @@ -86,13 +84,9 @@ function createPrompt(options: NormalizedSchema): Promise { ); } -export function promptAdditionalOptions(options: NormalizedSchema): Rule { - - return async () => { - const answers = await createPrompt(options); - options.platforms = answers?.platforms; - options.androidLanguage = answers?.androidLanguage; - options.iosLanguage = answers?.iosLanguage; - return; - }; +export async function promptAdditionalOptions(tree: Tree, options: NormalizedSchema) { + const answers = await createPrompt(options); + options.platforms = answers?.platforms; + options.androidLanguage = answers?.androidLanguage; + options.iosLanguage = answers?.iosLanguage; } diff --git a/packages/nx-flutter/src/schematics/application/schema.d.ts b/packages/nx-flutter/src/generators/application/schema.d.ts similarity index 84% rename from packages/nx-flutter/src/schematics/application/schema.d.ts rename to packages/nx-flutter/src/generators/application/schema.d.ts index 2b326abe..34620434 100644 --- a/packages/nx-flutter/src/schematics/application/schema.d.ts +++ b/packages/nx-flutter/src/generators/application/schema.d.ts @@ -6,7 +6,7 @@ export type IosLanguageType = 'objc' | 'swift'; export type TemplateType = 'app' | 'module' | 'package' | 'plugin'; export type PlatformType = 'android' | 'ios' | 'linux' | 'macos' | 'windows' | 'web'; -export interface ApplicationSchematicSchema { +export interface ApplicationGeneratorOptions { name: string; org?: string; description?: string; @@ -24,7 +24,7 @@ export interface ApplicationSchematicSchema { interactive?: boolean; } -export interface NormalizedSchema extends ApplicationSchematicSchema { +export interface NormalizedSchema extends ApplicationGeneratorOptions { projectName: string; projectRoot: string; projectDirectory: string; diff --git a/packages/nx-flutter/src/schematics/application/schema.json b/packages/nx-flutter/src/generators/application/schema.json similarity index 97% rename from packages/nx-flutter/src/schematics/application/schema.json rename to packages/nx-flutter/src/generators/application/schema.json index 1828eb88..32ff4759 100644 --- a/packages/nx-flutter/src/schematics/application/schema.json +++ b/packages/nx-flutter/src/generators/application/schema.json @@ -1,7 +1,8 @@ { "$schema": "http://json-schema.org/schema", "id": "NxFlutter", - "title": "", + "title": "Creates a Flutter project in the workspace", + "cli": "nx", "type": "object", "properties": { "name": { diff --git a/packages/nx-flutter/src/index.ts b/packages/nx-flutter/src/index.ts index e69de29b..83bf1bf3 100644 --- a/packages/nx-flutter/src/index.ts +++ b/packages/nx-flutter/src/index.ts @@ -0,0 +1 @@ +export { applicationGenerator } from './generators/application/generator'; \ No newline at end of file diff --git a/packages/nx-flutter/src/schematics/application/lib/generate-project.ts b/packages/nx-flutter/src/schematics/application/lib/generate-project.ts deleted file mode 100644 index 6f769c73..00000000 --- a/packages/nx-flutter/src/schematics/application/lib/generate-project.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - Rule, - SchematicContext, - Tree -} from '@angular-devkit/schematics'; -import { NormalizedSchema } from '../schema'; -import { generateFlutterProject } from '../../../utils/flutter-utils'; - - -export function generateProject(options: NormalizedSchema): Rule { - return (tree: Tree, context: SchematicContext) => { - return generateFlutterProject(options, tree, context); - }; -} diff --git a/packages/nx-flutter/src/schematics/application/lib/normalize-options.ts b/packages/nx-flutter/src/schematics/application/lib/normalize-options.ts deleted file mode 100644 index d13b3ca4..00000000 --- a/packages/nx-flutter/src/schematics/application/lib/normalize-options.ts +++ /dev/null @@ -1,33 +0,0 @@ - import { - projectRootDir, - ProjectType, - toFileName, - } from '@nrwl/workspace'; - import { ApplicationSchematicSchema, NormalizedSchema } from '../schema'; - - /** - * Depending on your needs, you can change this to either `Library` or `Application` - */ - const projectType = ProjectType.Application; - - export function normalizeOptions( - options: ApplicationSchematicSchema - ): NormalizedSchema { - const name = toFileName(options.name); - const projectDirectory = options.directory - ? `${toFileName(options.directory)}/${name}` - : name; - const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); - const projectRoot = `${projectRootDir(projectType)}/${projectDirectory}`; - const parsedTags = options.tags - ? options.tags.split(',').map((s) => s.trim()) - : []; - return { - ...options, - projectName, - projectRoot, - projectDirectory, - parsedTags, - }; - } - \ No newline at end of file diff --git a/packages/nx-flutter/src/schematics/application/schematic.spec.ts b/packages/nx-flutter/src/schematics/application/schematic.spec.ts deleted file mode 100644 index e0813446..00000000 --- a/packages/nx-flutter/src/schematics/application/schematic.spec.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { join } from 'path'; -import * as inquirer from 'inquirer'; -import { ApplicationSchematicSchema } from './schema'; - -jest.mock('child_process'); // we need to mock 'execSync' so that it doesn't really run 'flutter' (reserved to e2e testing) (see __mocks__/child_process.js) - -jest.mock('inquirer'); // we mock 'inquirer' to bypass the interactive prompt - - -const appCommands = [ - { key: 'assemble', value: 'assemble' }, - { key: 'attach', value: 'attach' }, - { key: 'drive', value: 'drive' }, - { key: 'genL10n', value: 'gen-l10n' }, - { key: 'install', value: 'install' }, - { key: 'run', value: 'run' }, -]; - -const pluginOrModOnlyCommands = [ - { key: 'buildAar', value: 'build aar'}, -]; - -const androidOnlyCommands = [ - { key: 'buildAar', value: 'build aar'}, - { key: 'buildApk', value: 'build apk' }, - { key: 'buildAppbundle', value: 'build appbundle' }, - { key: 'buildBundle', value: 'build bundle' }, -]; - -const iOsOnlyCommands = [ - { key: 'buildIos', value: 'build ios' }, - { key: 'buildIosFramework', value: 'build ios-framework' }, - { key: 'buildIpa', value: 'build ipa' }, -]; - -describe('application schematic', () => { - let appTree: Tree; - const defaultOptions: ApplicationSchematicSchema = { name: 'testapp' }; - - const testRunner = new SchematicTestRunner( - '@nxrocks/nx-flutter', - join(__dirname, '../../../collection.json') - ); - - beforeEach(() => { - appTree = createEmptyWorkspace(Tree.empty()); - - jest.spyOn(inquirer, 'prompt').mockResolvedValue({ - platforms: ['android', 'ios', 'web', 'linux', 'windows', 'macos'], - androidLanguage: 'kotlin', - iosLanguage: 'swift' - }); - }); - - afterEach(() => - (inquirer.prompt as jest.MockedFunction< - typeof inquirer.prompt - >).mockRestore() - ); - - it('should update workspace.json', async () => { - const tree = await testRunner - .runSchematicAsync('create', defaultOptions, appTree) - .toPromise(); - - const workspaceJson = readJsonInTree(tree, 'workspace.json'); - const root = 'apps/testapp'; - expect(workspaceJson.projects['testapp'].root).toBe(root); - - const commonCommands = [ - { key: 'analyze', value: 'analyze' }, - { key: 'clean', value: 'clean' }, - { key: 'format', value: `format ${root}/*` }, - { key: 'test', value: 'test' }, - ]; - - const commands = [...commonCommands, ...appCommands, ...pluginOrModOnlyCommands, ...androidOnlyCommands, ...iOsOnlyCommands]; - const architect = workspaceJson.projects['testapp'].architect; - - commands.forEach(e => { - expect(architect[e.key].builder).toBe('@nrwl/workspace:run-commands'); - expect(architect[e.key].options.command).toBe(`flutter ${e.value}`); - }); - }); - - it('should run successfully', async () => { - await expect( - testRunner.runSchematicAsync('application', defaultOptions, appTree).toPromise() - ).resolves.not.toThrowError(); - }); - - describe('generating flutter "app"', () => { - - it('should prompt user to select "platforms", "androidLanguage" and "iosLanguage"', async () => { - const tree = await testRunner - .runSchematicAsync('create', { ...defaultOptions, template: 'app' }, appTree) - .toPromise(); - - const workspaceJson = readJsonInTree(tree, 'workspace.json'); - const root = 'apps/testapp'; - expect(workspaceJson.projects['testapp'].root).toBe(root); - - expect(inquirer.prompt).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - when: true, - name: 'platforms', - type: 'checkbox', - choices: expect.arrayContaining([ - { - value: "android", - name: "Android platform", - checked: true, - }, - { - value: "ios", - name: "iOS platform", - checked: true, - }, - { - value: "linux", - name: "Linux platform", - checked: true, - }, - { - value: "windows", - name: "Windows platform", - checked: true, - }, - { - value: "macos", - name: "MacOS platform", - checked: true, - }, - { - value: "web", - name: "Web platform", - checked: true, - } - ]), - }), - expect.objectContaining({ - name: 'androidLanguage', - type: 'list', - default: 'kotlin', - choices: expect.arrayContaining([ - { - value: "java", - name: "Java" - }, - { - value: "kotlin", - name: "Kotlin" - } - ]), - message: "Which Android language would you like to use?", - }), - expect.objectContaining({ - name: 'iosLanguage', - type: 'list', - default: 'swift', - choices: expect.arrayContaining([ - { - value: "objc", - name: "Objective-C" - }, - { - value: "swift", - name: "Swift" - } - ]), - message: "Which iOS language would you like to use?", - }) - ]) - ); - }); - - }); - - describe('generating flutter "plugin"', () => { - - it('should prompt user to select "platforms", "androidLanguage" and "iosLanguage"', async () => { - const tree = await testRunner - .runSchematicAsync('create', { ...defaultOptions, template: 'plugin' }, appTree) - .toPromise(); - - const workspaceJson = readJsonInTree(tree, 'workspace.json'); - const root = 'apps/testapp'; - expect(workspaceJson.projects['testapp'].root).toBe(root); - - expect(inquirer.prompt).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - when: true, - name: 'platforms', - type: 'checkbox', - choices: expect.arrayContaining([ - { - value: "android", - name: "Android platform", - checked: true, - }, - { - value: "ios", - name: "iOS platform", - checked: true, - }, - { - value: "linux", - name: "Linux platform", - checked: true, - }, - { - value: "windows", - name: "Windows platform", - checked: true, - }, - { - value: "macos", - name: "MacOS platform", - checked: true, - }, - { - value: "web", - name: "Web platform", - checked: true, - } - ]), - }), - expect.objectContaining({ - name: 'androidLanguage', - type: 'list', - default: 'kotlin', - choices: expect.arrayContaining([ - { - value: "java", - name: "Java" - }, - { - value: "kotlin", - name: "Kotlin" - } - ]), - message: "Which Android language would you like to use?", - }), - expect.objectContaining({ - name: 'iosLanguage', - type: 'list', - default: 'swift', - choices: expect.arrayContaining([ - { - value: "objc", - name: "Objective-C" - }, - { - value: "swift", - name: "Swift" - } - ]), - message: "Which iOS language would you like to use?", - }) - ]) - ); - }); - - }); - - describe('generating flutter "module"', () => { - - it('should prompt user to select "platforms", "androidLanguage" and "iosLanguage"', async () => { - const tree = await testRunner - .runSchematicAsync('create', { ...defaultOptions, template: 'module' }, appTree) - .toPromise(); - - const workspaceJson = readJsonInTree(tree, 'workspace.json'); - const root = 'apps/testapp'; - expect(workspaceJson.projects['testapp'].root).toBe(root); - - expect(inquirer.prompt).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - when: false, - name: 'platforms', - type: 'checkbox', - choices: expect.arrayContaining([ - { - value: "android", - name: "Android platform", - checked: true, - }, - { - value: "ios", - name: "iOS platform", - checked: true, - }, - { - value: "linux", - name: "Linux platform", - checked: true, - }, - { - value: "windows", - name: "Windows platform", - checked: true, - }, - { - value: "macos", - name: "MacOS platform", - checked: true, - }, - { - value: "web", - name: "Web platform", - checked: true, - } - ]), - }), - expect.objectContaining({ - name: 'androidLanguage', - type: 'list', - default: 'kotlin', - choices: expect.arrayContaining([ - { - value: "java", - name: "Java" - }, - { - value: "kotlin", - name: "Kotlin" - } - ]), - message: "Which Android language would you like to use?", - }), - expect.objectContaining({ - name: 'iosLanguage', - type: 'list', - default: 'swift', - choices: expect.arrayContaining([ - { - value: "objc", - name: "Objective-C" - }, - { - value: "swift", - name: "Swift" - } - ]), - message: "Which iOS language would you like to use?", - }) - ]) - ); - }); - - }); - - describe('generating flutter "package"', () => { - - it('should prompt user to select "platforms", "androidLanguage" and "iosLanguage"', async () => { - const tree = await testRunner - .runSchematicAsync('create', { ...defaultOptions, template: 'package' }, appTree) - .toPromise(); - - const workspaceJson = readJsonInTree(tree, 'workspace.json'); - const root = 'apps/testapp'; - expect(workspaceJson.projects['testapp'].root).toBe(root); - - expect(inquirer.prompt).toBeCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - when: false, - name: 'platforms', - type: 'checkbox', - choices: expect.arrayContaining([ - { - value: "android", - name: "Android platform", - checked: true, - }, - { - value: "ios", - name: "iOS platform", - checked: true, - }, - { - value: "linux", - name: "Linux platform", - checked: true, - }, - { - value: "windows", - name: "Windows platform", - checked: true, - }, - { - value: "macos", - name: "MacOS platform", - checked: true, - }, - { - value: "web", - name: "Web platform", - checked: true, - } - ]), - }), - expect.objectContaining({ - name: 'androidLanguage', - type: 'list', - default: 'kotlin', - choices: expect.arrayContaining([ - { - value: "java", - name: "Java" - }, - { - value: "kotlin", - name: "Kotlin" - } - ]), - message: "Which Android language would you like to use?", - }), - expect.objectContaining({ - name: 'iosLanguage', - type: 'list', - default: 'swift', - choices: expect.arrayContaining([ - { - value: "objc", - name: "Objective-C" - }, - { - value: "swift", - name: "Swift" - } - ]), - message: "Which iOS language would you like to use?", - }) - ]) - ); - }); - - }); -}); diff --git a/packages/nx-flutter/src/schematics/application/schematic.ts b/packages/nx-flutter/src/schematics/application/schematic.ts deleted file mode 100644 index 8c8b555d..00000000 --- a/packages/nx-flutter/src/schematics/application/schematic.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - noop, - chain, - Rule, -} from '@angular-devkit/schematics'; -import { - addProjectToNxJsonInTree, - ProjectType, - updateWorkspace, -} from '@nrwl/workspace'; -import { isFlutterInstalled } from '../../utils/flutter-utils'; -import { promptAdditionalOptions } from './lib/prompt-options'; -import { generateProject } from './lib/generate-project'; -import { normalizeOptions } from './lib/normalize-options'; -import { ApplicationSchematicSchema } from './schema'; - -/** - * Depending on your needs, you can change this to either `Library` or `Application` - */ -const projectType = ProjectType.Application; - -export default function (options: ApplicationSchematicSchema): Rule { - - if (!isFlutterInstalled()) { - throw new Error("'flutter' was not found on your system's PATH.\nPlease make sure you have installed it correctly.\nšŸ‘‰šŸ¾ https://flutter.dev/docs/get-started/install"); - } - - const normalizedOptions = normalizeOptions(options); - return chain([ - options.interactive ? promptAdditionalOptions(normalizedOptions): noop(), - updateWorkspace((workspace) => { - const project = workspace.projects - .add({ - name: normalizedOptions.projectName, - root: normalizedOptions.projectRoot, - sourceRoot: `${normalizedOptions.projectRoot}/src`, - projectType, - }); - - const commands = [ - { key: 'analyze', value: 'analyze' }, - { key: 'clean', value: 'clean' }, - { key: 'format', value: `format ${normalizedOptions.projectRoot}/*` }, - { key: 'test', value: 'test' }, - ]; - - if(normalizedOptions.template === 'app'){ - commands.push( - { key: 'assemble', value: 'assemble' }, - { key: 'attach', value: 'attach' }, - { key: 'drive', value: 'drive' }, - { key: 'genL10n', value: 'gen-l10n' }, - { key: 'install', value: 'install' }, - { key: 'run', value: 'run' }, - ) - } - - if(normalizedOptions.platforms?.indexOf('android') != -1) { - commands.push( - {key: 'buildAar', value: 'build aar'}, - {key: 'buildApk', value: 'build apk'}, - {key: 'buildAppbundle', value: 'build appbundle'}, - {key: 'buildBundle', value: 'build bundle'}, - ) - } - - if(normalizedOptions.platforms?.indexOf('ios') != -1) { - commands.push( - {key: 'buildIos', value: 'build ios'}, - {key: 'buildIosFramework', value: 'build ios-framework'}, - {key: 'buildIpa', value: 'build ipa'}, - ) - } - - commands.forEach(e => { - project.targets.add({ - name: `${e.key}`, - builder: '@nrwl/workspace:run-commands', - options: { - command: `flutter ${e.value}`, - cwd : normalizedOptions.projectRoot - } - }); - }); - }), - addProjectToNxJsonInTree(normalizedOptions.projectName, { - tags: normalizedOptions.parsedTags, - }), - generateProject(normalizedOptions) - ]); -} diff --git a/packages/nx-flutter/src/utils/flutter-utils.ts b/packages/nx-flutter/src/utils/flutter-utils.ts index b2190151..9767d826 100644 --- a/packages/nx-flutter/src/utils/flutter-utils.ts +++ b/packages/nx-flutter/src/utils/flutter-utils.ts @@ -1,6 +1,5 @@ -import { Tree, SchematicContext} from '@angular-devkit/schematics'; import { execSync } from 'child_process' -import { NormalizedSchema } from '../schematics/application/schema'; +import { NormalizedSchema } from '../generators/application/schema'; export function isFlutterInstalled(): boolean { try { @@ -49,21 +48,3 @@ export function buildFlutterCreateOptions(options: NormalizedSchema) { return opts; } - -export async function generateFlutterProject(options: NormalizedSchema, tree: Tree, context: SchematicContext): Promise { - const opts = this.buildFlutterCreateOptions(options); - - context.logger.info(`Generating Flutter project with following options : ${opts}...`); - - // Create the command to execute - const execute = `flutter create ${opts} ${options.projectRoot}`; - try { - context.logger.info(`Executing command: ${execute}`); - execSync(execute, { stdio: [0, 1, 2] }); - return ; - } catch (e) { - context.logger.error(`Failed to execute command: ${execute}`, e); - return ; - } -} - diff --git a/workspace.json b/workspace.json index 1aca2fa3..4c56e676 100644 --- a/workspace.json +++ b/workspace.json @@ -120,12 +120,12 @@ }, { "input": "./packages/nx-flutter", - "glob": "collection.json", + "glob": "generators.json", "output": "." }, { "input": "./packages/nx-flutter", - "glob": "builders.json", + "glob": "executors.json", "output": "." } ]