Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Suggestions for executors #127

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
659 changes: 574 additions & 85 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"tslib": "^2.0.0",
"typescript": "4.8.4"
"typescript": "5.1.6"
},
"dependencies": {
"@inquirer/prompts": "^2.3.0",
"parse-help": "^1.0.0",
"semver": "^7.3.8",
"tslib": "^2.0.0"
"tslib": "^2.0.0",
"yargs": "^17.7.2"
}
}
57 changes: 57 additions & 0 deletions packages/nx-firebase/src/executors/cli/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ExecutorContext, logger } from '@nx/devkit'
import { CliExecutorSchema } from './schema'
import {
isString,
normalizeOptions,
runCommands,
stringifyFirebaseArgs,
} from './utils'

/**
* @param options
* @param context
* @returns
*/
export default async function runExecutor(
options: CliExecutorSchema & { [key: string]: unknown },
context: Pick<ExecutorContext, 'root'>,
) {
logger.info('Starting Executor...')
const normalizedOptions = await normalizeOptions(options, context)
if (!normalizedOptions.args.project) return { success: false }

const stringifiedArgs = stringifyFirebaseArgs(
normalizedOptions._[0],
normalizedOptions.args,
)

//:TODO remove
if (normalizedOptions._[0].includes('emulators')) {
if (isString(normalizedOptions.args.only)) {
if (normalizedOptions.args.only.includes('auth')) {
process.env.NX_REACT_APP_AUTH_EMULATOR = 'true'
}
if (normalizedOptions.args.only.includes('functions')) {
process.env.NX_REACT_APP_FUNCTIONS_EMULATOR = 'true'
}
if (normalizedOptions.args.only.includes('firestore')) {
process.env.NX_REACT_APP_FIRESTORE_EMULATOR = 'true'
}
if (normalizedOptions.args.only.includes('storage')) {
process.env.NX_REACT_APP_STORAGE_EMULATOR = 'true'
}
} else {
process.env.NX_REACT_APP_AUTH_EMULATOR = 'true'
process.env.NX_REACT_APP_FUNCTIONS_EMULATOR = 'true'
process.env.NX_REACT_APP_FIRESTORE_EMULATOR = 'true'
process.env.NX_REACT_APP_STORAGE_EMULATOR = 'true'
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my current workaround for detecting if my frontend application should connect to the emulators when serving, but as you can see it is specific to cra applications. The frontend code then looks for these variables before in initialises each sdk.

Not sure if there is a more generic way of doing this/if it should be removed and handled elsewhere


const command = ['firebase', ...normalizedOptions._, stringifiedArgs]
.filter(Boolean)
.join(' ')

runCommands([command])
return { success: true }
}
6 changes: 6 additions & 0 deletions packages/nx-firebase/src/executors/cli/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface CliExecutorSchema {
command: string
config: string
project?: string
_: string[]
}
36 changes: 36 additions & 0 deletions packages/nx-firebase/src/executors/cli/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "Firebase CLI executor",
"description": "Runs the Firebase CLI for the target project's firebase configuration",
"type": "object",
"properties": {
"config": {
"type": "string",
"description": "Path to the firebase configuration json for this project",
"default": "firebase.json"
},
"command": {
"description": "Firebase CLI command",
"type": "string",
"format": "html-selector",
"$default": {
"$source": "argv",
"index": 0
}
},
"project": {
"type": "string",
"description": "Name or alias from the .firebaserc of the firebase project being used"
},
"_": {
"type": "array",
"items": {
"anyOf": [{ "type": "string" }]
},
"description": "Non-hyphenated options for firebase command",
"default": []
}
},
"required": ["config", "command"]
}
165 changes: 165 additions & 0 deletions packages/nx-firebase/src/executors/cli/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { execSync } from 'node:child_process'
import { setTimeout } from 'node:timers/promises'
import { Separator, select } from '@inquirer/prompts'
import { ExecutorContext, joinPathFragments, logger } from '@nx/devkit'
import parseHelp from 'parse-help'
import yargs from 'yargs/yargs'
import { CliExecutorSchema } from './schema'

export const parseCommand = (command: string): string => {
if (command[0] === '"' && command.at(-1) === '"') {
return command.slice(1, -1)
}
return command
}

export const runCommand = (command: string): void => {
execSync(parseCommand(command), {
stdio: 'inherit',
})
}

export const runCommands = (commands: string[]): void => {
for (const command of commands) {
runCommand(command)
}
}

export const isString = (u: unknown): u is string => {
if (typeof u === 'string') return true
return false
}

const getFirebaseProject = async (
project?: string,
): Promise<string | undefined> => {
if (project) return project
let activeFirebaseProject = execSync(
`echo $(echo $(grep "$(pwd)" ~/.config/configstore/firebase-tools.json | cut -d" " -f2)" " | sed -e 's/"//g')`,
)
.toString()
.trim()

if (!activeFirebaseProject) {
logger.error(
`No firebase project has been explicitly set and No active project. Please select desired project with 'firebase use' or select project when running command`,
)

return undefined
}

if (activeFirebaseProject.at(-1) === ',') {
activeFirebaseProject = activeFirebaseProject.slice(0, -1)
}

const useActiveProject = select({
message: `Use active project (${activeFirebaseProject})?`,
choices: [
{
name: 'Yes',
value: true,
},
{
name: 'No',
value: false,
},
new Separator(),
],
})

const defaultUseActiveProject = setTimeout(10000).then(() => {
useActiveProject.cancel()
return false
})

const answer = await Promise.race([defaultUseActiveProject, useActiveProject])

if (!answer) {
logger.error(
`Select desired project with 'firebase use' or select project when running command`,
)
return undefined
}

return activeFirebaseProject
}

export const normalizeOptions = async (
options: CliExecutorSchema & {
[key: string]: unknown
},
context: Pick<ExecutorContext, 'root'>,
): Promise<{ _: string[]; args: Record<string, unknown> }> => {
const { command, config, _: params, project, ...additionalArgs } = options

const { $0, _: commandParams, ...commandArgs } = await yargs(command).argv

const [cCommand, ...cParams] = commandParams.map((value) => {
return value.toString()
})

if (cCommand === params[0]) {
params.shift()
}

if (cParams.length > 0 && params.length > 0) {
logger.warn('Conflicting parameters for firebase command')
logger.warn(`Parameters to ignore [${cParams.toString()}]`)
logger.warn(`Parameters to use [${params.toString()}]`)
}

const finalParams = [
cCommand,
...(params ?? cParams).map((value) => {
if (value[0] !== '"' && value.at(-1) !== '"' && value.includes(' ')) {
return '"' + value + '"'
}
return value
}),
].filter(Boolean)

//Set up args
const finalArgs = {
...commandArgs,
...additionalArgs,
}

const finalArgsProject =
typeof finalArgs.project === 'string' ? finalArgs.project : undefined

const firebaseProject = await getFirebaseProject(project ?? finalArgsProject)
finalArgs.project = firebaseProject
finalArgs.config = joinPathFragments(context.root, config)

return {
_: finalParams,
args: finalArgs,
}
}

export const stringifyFirebaseArgs = (
command: string,
args: Record<string, unknown>,
): string => {
const base = parseHelp(execSync('firebase --help'))
const base2 = parseHelp(execSync(`firebase ${command} --help`))

const validArgs = Object.keys({
...base.flags,
...base.aliases,
...base2.flags,
...base2.aliases,
})

const argsToBeUsed = Object.keys(args)
.filter((p) => validArgs.indexOf(p) !== -1)
.reduce((m, c) => ((m[c] = args[c]), m), {})

return Object.keys(argsToBeUsed)
.map((a) =>
argsToBeUsed[a] === true || argsToBeUsed[a] === 'true'
? `--${a}`
: `--${a}=${argsToBeUsed[a]}`,
)
.join(' ')
}
36 changes: 36 additions & 0 deletions packages/nx-firebase/src/executors/copy-local-files/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { copyFileSync, statSync } from 'fs'
import path from 'path'
import { ExecutorContext, joinPathFragments, logger } from '@nx/devkit'
import { CopyLocalFilesExecutorSchema } from './schema'

export default async function runExecutor(
options: CopyLocalFilesExecutorSchema,
context: ExecutorContext
) {
const projectName = context.projectName
if (!projectName) return { success: false }
const projectRoot = context.workspace?.projects[projectName].root
const projectRootPath = `${context.root}/${projectRoot}`
const projectDistPath = `${context.root}/dist/${projectRoot}`

for (const file of ['.env.local', '.secret.local']) {
const fileFullPath = joinPathFragments(projectRootPath, file)
if (fileExists(fileFullPath)) {
copyFileSync(fileFullPath, joinPathFragments(projectDistPath, file))
}
}
return {
success: true,
}
}

const fileExists = (path: string): boolean => {
try {
const isFile = statSync(path).isFile()
if (!isFile) logger.warn(`'${path}' is not a file`)
return isFile
} catch (error) {
logger.warn(error.message)
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface CopyLocalFilesExecutorSchema {} // eslint-disable-line
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "CopyLocalFiles executor",
"description": "",
"type": "object",
"properties": {},
"required": []
}
Empty file.
47 changes: 47 additions & 0 deletions packages/nx-firebase/src/executors/kill-emulator/executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { execSync } from 'child_process'
import { platform } from 'os'
import { ExecutorContext, joinPathFragments, logger, readJsonFile } from '@nx/devkit'
import { KillEmulatorPortsExecutorSchema } from './schema.js'

export default async function runExecutor(
options: KillEmulatorPortsExecutorSchema,
context: ExecutorContext
) {
const normalizedPath = joinPathFragments(context.root, options.config)
const emulatorJson = readJsonFile(normalizedPath)

const emulators = emulatorJson.emulators as Record<string, unknown>

const emulatorPorts: number[] = []
for (const entry of Object.entries(emulators)) {
const value = entry[1]
if (typeof value === 'object' && value && 'port' in value) {
const port = Number(value.port)
if (!isNaN(port)) emulatorPorts.push(port)
}
}

const killFunc = platform() === 'win32' ? win32Kill : unixKill
emulatorPorts.map(killFunc)

return {
success: true,
}
}

function win32Kill(port: number) {
logger.log(
`${port} not killed... killing emulators through this method is not implemented for windows`
)
}

function unixKill(port: number) {
const pid = execSync(`lsof -t -i:${port} || echo ""`).toString().trim()
if (!pid) {
console.log(`no process running at port ${port}`)
return
}
logger.log(`killing process at port ${port}...`)
execSync(`kill -9 ${pid}`)
logger.log(`killed process at port ${port}`)
}
3 changes: 3 additions & 0 deletions packages/nx-firebase/src/executors/kill-emulator/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface KillEmulatorPortsExecutorSchema {
config: string
}
14 changes: 14 additions & 0 deletions packages/nx-firebase/src/executors/kill-emulator/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "KillEmulatorPorts executor",
"description": "",
"type": "object",
"properties": {
"config": {
"type": "string",
"description": "Path to project's firebase config (where the emulator ports are defined)"
}
},
"required": ["config"]
}
Loading