@entur/function-tools 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { registerStart } from '../lib/commands/start.js';
5
5
  import { registerUnusedExports } from '../lib/commands/unusedExports.js';
6
6
 
7
7
  const program = new Command();
8
- program.name("entur-functions").description("A multi-tool for Firebase functions at Entur").version("0.0.4").option("-v, --verbose", "Enable verbose output");
8
+ program.name("entur-functions").description("A multi-tool for Firebase functions at Entur").version("0.0.6").option("-v, --verbose", "Enable verbose output");
9
9
  registerBuild(program);
10
10
  registerDeploy(program);
11
11
  registerStart(program);
@@ -0,0 +1,88 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { builtinModules } from 'node:module';
3
+ import { dirname } from 'node:path';
4
+ import { stringify } from 'yaml';
5
+ import { groupByAsync, asyncMap } from '../utils/async.js';
6
+ import { spawnAsync } from '../utils/exec.js';
7
+ import { writePackageJSON, readPackageJSON, packageUp } from '../utils/packageJSON.js';
8
+
9
+ const alwaysIncludePackageNames = [
10
+ "@google-cloud/functions-framework",
11
+ "firebase-functions",
12
+ "firebase-admin"
13
+ ];
14
+ async function writeDependencies(outputs, getOutputFilePath, outputDir) {
15
+ const packages = [];
16
+ for (const [packageJSONPath, packageJSON] of (await calculateDependencies(outputs))){
17
+ const newPackageJSONPath = getOutputFilePath(packageJSONPath);
18
+ const directory = dirname(newPackageJSONPath);
19
+ if (directory === ".") {
20
+ await writePackageJSON(new URL(`./${newPackageJSONPath}`, outputDir), {
21
+ ...packageJSON,
22
+ main: `./${outputs[0].fileName}`
23
+ });
24
+ } else {
25
+ await writePackageJSON(new URL(`./${newPackageJSONPath}`, outputDir), packageJSON);
26
+ packages.push(directory);
27
+ }
28
+ }
29
+ await writeFile(new URL("./pnpm-workspace.yaml", outputDir), stringify({
30
+ packages
31
+ }));
32
+ }
33
+ async function calculateDependencies(outputs) {
34
+ const outputGroupedByPackage = await groupByAsync(outputs.filter((it)=>Boolean(it.type === "chunk" && it.facadeModuleId && it.imports.length > 0)), async (output)=>{
35
+ const key = output.facadeModuleId && await packageUp({
36
+ cwd: output.facadeModuleId
37
+ });
38
+ if (!key) {
39
+ throw new Error(`No package.json found for ${output.facadeModuleId}`);
40
+ }
41
+ return key;
42
+ });
43
+ const filterDependencies = (lookup, dependencies)=>{
44
+ if (!dependencies) return;
45
+ const entries = Object.entries(dependencies).filter(([key])=>lookup.has(key));
46
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
47
+ };
48
+ const entries = await asyncMap([
49
+ ...outputGroupedByPackage
50
+ ], async ([packageJSONPath, outputs])=>{
51
+ const { name, type, version, ...packageJSON } = await readPackageJSON(packageJSONPath);
52
+ const importedPackageNames = new Set(outputs.flatMap((it)=>it.imports).filter((it)=>!isBuiltinImport(it)).map(specifierToPackageName));
53
+ alwaysIncludePackageNames?.forEach((it)=>{
54
+ importedPackageNames.add(it);
55
+ });
56
+ return [
57
+ packageJSONPath,
58
+ {
59
+ name,
60
+ type,
61
+ version,
62
+ dependencies: filterDependencies(importedPackageNames, packageJSON.dependencies),
63
+ devDependencies: filterDependencies(importedPackageNames, packageJSON.devDependencies),
64
+ optionalDependencies: filterDependencies(importedPackageNames, packageJSON.optionalDependencies)
65
+ }
66
+ ];
67
+ });
68
+ return new Map(entries);
69
+ }
70
+ function specifierToPackageName(specifier) {
71
+ if (specifier.startsWith("@")) {
72
+ return specifier.split("/", 2).join("/");
73
+ }
74
+ return specifier.split("/", 1).join("/");
75
+ }
76
+ function isBuiltinImport(specifier) {
77
+ return specifier.startsWith("node:") || builtinModules.includes(specifier);
78
+ }
79
+ async function linkDependencies(workingDir) {
80
+ await spawnAsync("pnpm", [
81
+ "install",
82
+ "--prefer-offline"
83
+ ], {
84
+ cwd: workingDir
85
+ });
86
+ }
87
+
88
+ export { linkDependencies, writeDependencies };
@@ -2,29 +2,35 @@ import { fileURLToPath } from 'node:url';
2
2
  import { nodeResolve } from '@rollup/plugin-node-resolve';
3
3
  import swc from '@rollup/plugin-swc';
4
4
  import { rollup, watch } from 'rollup';
5
+ import { createOutputFileName } from './utils.js';
5
6
 
6
- async function bundle(entryFile, outputDir, { packagesToInline } = {}) {
7
- const bundle1 = await rollup({
7
+ async function bundle(entryFile, { outputDir, packageRoot, projectRoot, packagesToInline }) {
8
+ const bundle = await rollup({
8
9
  input: fileURLToPath(entryFile),
9
10
  plugins: plugins(packagesToInline),
10
11
  treeshake: "smallest"
11
12
  });
12
- return bundle1.write({
13
- ...outputOptions(),
14
- sourcemap: true,
15
- dir: fileURLToPath(outputDir)
13
+ const outputFileName = createOutputFileName(packageRoot, projectRoot);
14
+ return bundle.write({
15
+ preserveModules: true,
16
+ format: "esm",
17
+ dir: fileURLToPath(outputDir),
18
+ entryFileNames: ({ facadeModuleId })=>outputFileName(facadeModuleId)
16
19
  });
17
20
  }
18
- async function bundleAndWatch(entryFile, outputDir, { packagesToInline, onBundleEnd }) {
21
+ async function bundleAndWatch(entryFile, { outputDir, packageRoot, projectRoot, packagesToInline, onBundleEnd }) {
22
+ const outputFileName = createOutputFileName(packageRoot, projectRoot);
23
+ const output = {
24
+ preserveModules: true,
25
+ format: "esm",
26
+ dir: fileURLToPath(outputDir),
27
+ entryFileNames: ({ facadeModuleId })=>outputFileName(facadeModuleId)
28
+ };
19
29
  const watchOptions = {
20
30
  plugins: plugins(packagesToInline),
21
31
  input: fileURLToPath(entryFile),
22
32
  treeshake: "smallest",
23
- output: {
24
- ...outputOptions(),
25
- sourcemap: true,
26
- dir: fileURLToPath(outputDir)
27
- },
33
+ output,
28
34
  watch: {
29
35
  exclude: [
30
36
  fileURLToPath(outputDir)
@@ -41,9 +47,9 @@ async function bundleAndWatch(entryFile, outputDir, { packagesToInline, onBundle
41
47
  }
42
48
  case "BUNDLE_END":
43
49
  {
44
- const output = await event.result.generate(outputOptions());
50
+ const bundledOutput = await event.result.generate(output);
45
51
  console.log(`🏠 Finished build in ${event.duration}ms`);
46
- await onBundleEnd?.(output);
52
+ await onBundleEnd?.(bundledOutput);
47
53
  return event.result.close();
48
54
  }
49
55
  case "ERROR":
@@ -67,11 +73,5 @@ const plugins = (workspacePackages)=>[
67
73
  resolveOnly: (moduleId)=>moduleId.startsWith(".") || workspacePackages?.some((it)=>moduleId.startsWith(it)) || moduleId.startsWith("@entur-private/") || false
68
74
  })
69
75
  ];
70
- const outputOptions = ()=>({
71
- preserveModules: true,
72
- dir: "lib",
73
- format: "esm",
74
- entryFileNames: ({ isEntry })=>isEntry ? "index.js" : "[hash].js"
75
- });
76
76
 
77
77
  export { bundle, bundleAndWatch };
@@ -0,0 +1,23 @@
1
+ import { fileURLToPath } from 'node:url';
2
+
3
+ const nodeModulesPartPath = "/node_modules/";
4
+ function createOutputFileName(packageRootURL, projectRootURL) {
5
+ const projectRoot = fileURLToPath(projectRootURL);
6
+ const packageRoot = fileURLToPath(packageRootURL);
7
+ return (facadeModuleId)=>{
8
+ const index = facadeModuleId?.lastIndexOf(nodeModulesPartPath);
9
+ if (facadeModuleId && index && index > -1) {
10
+ return `bundled_modules/${facadeModuleId.slice(index + nodeModulesPartPath.length)}`;
11
+ }
12
+ const filePath = facadeModuleId?.replace(".ts", ".js").replaceAll("/src/", "/lib/");
13
+ if (filePath?.startsWith(packageRoot)) {
14
+ return filePath.slice(packageRoot.length);
15
+ }
16
+ if (filePath?.startsWith(projectRoot)) {
17
+ return `local_modules/${filePath.slice(projectRoot.length)}`;
18
+ }
19
+ throw new Error(`Unable to determine correct placement for file ${facadeModuleId}`);
20
+ };
21
+ }
22
+
23
+ export { createOutputFileName };
@@ -1,5 +1,6 @@
1
- import { bundle } from '../utils/bundle.js';
2
- import { flattenDependencies, harmonizeDependencies, calculateDependencies } from '../utils/dependencies.js';
1
+ import { writeDependencies } from '../bundle/dependencies.js';
2
+ import { bundle } from '../bundle/index.js';
3
+ import { createOutputFileName } from '../bundle/utils.js';
3
4
  import { cleanDir } from '../utils/fs.js';
4
5
  import { readPackageJSON } from '../utils/packageJSON.js';
5
6
  import { getWorkspacePackageNames } from '../utils/workspace.js';
@@ -8,31 +9,29 @@ import { resolveProjectConfig } from './utils.js';
8
9
  function registerBuild(program) {
9
10
  program.command("build").description("Build the project").option("-o, --output-dir <dir>", "Output directory").action(async (options)=>{
10
11
  try {
11
- const { packageRoot, packageJSON, outputDir, pnpmWorkspaceYAML } = await resolveProjectConfig(options);
12
+ const projectConfig = await resolveProjectConfig(options);
13
+ const { packageRoot, packageJSON, outputDir, pnpmWorkspaceYAML } = projectConfig;
12
14
  const { name, exports: exports$1 } = await readPackageJSON(packageJSON);
13
15
  console.log("🧹 Cleaning dist folder");
14
16
  await cleanDir(outputDir);
15
17
  console.log(`🔨 Building ${name}`);
16
18
  const workspacePackages = await getWorkspacePackageNames(pnpmWorkspaceYAML);
17
19
  const entryFile = new URL(exports$1?.["."] ?? "./index.js", packageRoot);
18
- await build(entryFile, outputDir, workspacePackages);
20
+ await build(entryFile, workspacePackages, projectConfig);
19
21
  } catch (error) {
20
22
  console.error(error);
21
23
  process.exit(1);
22
24
  }
23
25
  });
24
26
  }
25
- async function build(entryFile, outputDir, packagesToInline) {
26
- const bundleOutputDir = new URL("lib", outputDir);
27
- const { output } = await bundle(entryFile, bundleOutputDir, {
27
+ async function build(entryFile, packagesToInline, { outputDir, packageRoot, projectRoot }) {
28
+ const { output } = await bundle(entryFile, {
29
+ outputDir,
30
+ packageRoot,
31
+ projectRoot,
28
32
  packagesToInline
29
33
  });
30
- const dependencies = await flattenDependencies(harmonizeDependencies(await calculateDependencies(output)));
31
- // const { contentHash } = await pack(functionGroupDir, distDir)
32
- return {
33
- main: `./lib/${output[0].fileName}`,
34
- dependencies
35
- };
34
+ await writeDependencies(output, createOutputFileName(packageRoot, projectRoot), outputDir);
36
35
  }
37
36
 
38
37
  export { registerBuild };
@@ -1,12 +1,12 @@
1
- import { writeFile } from 'node:fs/promises';
2
- import { bundle } from '../utils/bundle.js';
3
- import { flattenDependencies, harmonizeDependencies, calculateDependencies, linkDependencies } from '../utils/dependencies.js';
4
- import { spawnAsync } from '../utils/exec.js';
1
+ import { writeDependencies, linkDependencies } from '../bundle/dependencies.js';
2
+ import { bundle } from '../bundle/index.js';
3
+ import { createOutputFileName } from '../bundle/utils.js';
4
+ import { deploy as deploy$1 } from '../firebase/index.js';
5
5
  import { getFirebaseJSON, writeFirebaseJSON } from '../utils/firebase.js';
6
6
  import { cleanDir, writeJSON } from '../utils/fs.js';
7
- import { readPackageJSON, getPackageName, writePackageJSON } from '../utils/packageJSON.js';
7
+ import { readPackageJSON, getPackageName } from '../utils/packageJSON.js';
8
8
  import { getWorkspacePackageNames } from '../utils/workspace.js';
9
- import { resolveProjectConfig } from './utils.js';
9
+ import { resolveProjectConfig, copyEnvFiles } from './utils.js';
10
10
 
11
11
  function registerDeploy(program) {
12
12
  program.command("deploy").description("Deploy the project").option("-o, --output-dir <dir>", "Output directory").option("-P, --project <project id or alias>", "Project id or alias").action(async (options)=>{
@@ -19,45 +19,39 @@ function registerDeploy(program) {
19
19
  }
20
20
  });
21
21
  }
22
- async function deploy({ projectAlias, projectId, packageJSON, packageRoot, pnpmWorkspaceYAML, projectRoot, outputDir }) {
23
- const { name, version, exports: exports$1 } = await readPackageJSON(packageJSON);
22
+ async function deploy(projectConfig) {
23
+ const { projectAlias, projectId, packageJSON, packageRoot, pnpmWorkspaceYAML, projectRoot, outputDir } = projectConfig;
24
+ const { name, exports: exports$1 } = await readPackageJSON(packageJSON);
24
25
  const workspacePackages = await getWorkspacePackageNames(pnpmWorkspaceYAML);
25
26
  const entryFile = new URL(exports$1?.["."] ?? "./index.js", packageRoot);
26
27
  console.log("🧹 Cleaning dist folder");
27
28
  await cleanDir(outputDir);
28
29
  console.log(`🔨 Building ${name}`);
29
- const { main, dependencies } = await build(entryFile, outputDir, workspacePackages);
30
+ await build(entryFile, workspacePackages, projectConfig);
30
31
  console.log("⛷️ Prepare deploy");
31
32
  const firebaseJSON = await getFirebaseJSON(new URL("firebase.json", projectRoot));
32
- await prepareDeploy(getPackageName(name), projectAlias, projectId, {
33
- name,
34
- type: "module",
35
- version,
36
- main,
37
- ...dependencies
38
- }, firebaseJSON, outputDir);
39
- // contentHash,
33
+ await prepareDeploy(getPackageName(name), firebaseJSON, projectConfig);
40
34
  console.log(`🚢 Deploying to ${projectAlias} (${projectId})`);
41
- await deployToFirebase(outputDir, projectId);
35
+ await deploy$1({
36
+ projectId,
37
+ cwd: outputDir
38
+ });
42
39
  }
43
- async function build(entryFile, outputDir, packagesToInline) {
44
- const bundleOutputDir = new URL("lib", outputDir);
45
- const { output } = await bundle(entryFile, bundleOutputDir, {
40
+ async function build(entryFile, packagesToInline, { outputDir, packageRoot, projectRoot }) {
41
+ const { output } = await bundle(entryFile, {
42
+ packageRoot,
43
+ projectRoot,
44
+ outputDir,
46
45
  packagesToInline
47
46
  });
48
- const dependencies = await flattenDependencies(harmonizeDependencies(await calculateDependencies(output)));
49
- // const { contentHash } = await pack(functionGroupDir, distDir)
50
- return {
51
- main: `./lib/${output[0].fileName}`,
52
- dependencies
53
- };
47
+ await writeDependencies(output, createOutputFileName(packageRoot, projectRoot), outputDir);
54
48
  }
55
- async function prepareDeploy(codebase, projectAlias, projectId, packageJSON, firebaseJSON, outputDir) {
49
+ async function prepareDeploy(codebase, firebaseJSON, { projectAlias, projectId, outputDir, envFiles }) {
50
+ const envPrefix = `FUNCTION_CODEBASE='${codebase}'\nENTUR_PROJECT_ALIAS='${projectAlias}'`;
56
51
  await Promise.all([
57
52
  writeFirebaseJSON(codebase, firebaseJSON, outputDir),
58
53
  createFirebaseRC(projectAlias, projectId, outputDir),
59
- writePackageJSON(new URL("./package.json", outputDir), packageJSON),
60
- createDotenv(codebase, projectAlias, outputDir)
54
+ copyEnvFiles(envFiles, envPrefix, outputDir)
61
55
  ]);
62
56
  await linkDependencies(outputDir);
63
57
  }
@@ -68,24 +62,5 @@ async function createFirebaseRC(projectAlias, projectId, outputDir) {
68
62
  }
69
63
  });
70
64
  }
71
- function deployToFirebase(workingDir, projectId, extraArgs = []) {
72
- return spawnAsync("firebase", [
73
- "deploy",
74
- "--only",
75
- "functions,firestore",
76
- "-P",
77
- projectId,
78
- ...extraArgs
79
- ], {
80
- cwd: workingDir
81
- });
82
- }
83
- async function createDotenv(codebase, projectAlias, outputDir) {
84
- const envFile = `
85
- NODE_OPTIONS='--enable-source-maps'
86
- FUNCTION_CODEBASE='${codebase}'
87
- ENTUR_PROJECT_ALIAS='${projectAlias}'`;
88
- await writeFile(new URL("./.env", outputDir), envFile.trim());
89
- }
90
65
 
91
66
  export { registerDeploy };
@@ -1,63 +1,66 @@
1
- import { bundleAndWatch } from '../utils/bundle.js';
2
- import { flattenDependencies, harmonizeDependencies, calculateDependencies, linkDependencies } from '../utils/dependencies.js';
3
- import { spawnAsync } from '../utils/exec.js';
1
+ import { writeDependencies, linkDependencies } from '../bundle/dependencies.js';
2
+ import { bundleAndWatch } from '../bundle/index.js';
3
+ import { createOutputFileName } from '../bundle/utils.js';
4
+ import { startEmulator } from '../firebase/index.js';
4
5
  import { getFirebaseJSON, writeFirebaseJSON } from '../utils/firebase.js';
5
6
  import { cleanDir, writeJSON } from '../utils/fs.js';
6
- import { readPackageJSON, getPackageName, writePackageJSON } from '../utils/packageJSON.js';
7
+ import { readPackageJSON, getPackageName } from '../utils/packageJSON.js';
7
8
  import { getWorkspacePackageNames } from '../utils/workspace.js';
8
- import { resolveProjectConfig } from './utils.js';
9
+ import { resolveProjectConfig, copyEnvFiles } from './utils.js';
9
10
 
10
11
  function registerStart(program) {
11
- program.command("start").description("Start function emulator").option("-o, --output-dir <dir>", "Output directory").option("-P, --project <project id or alias>", "Project id or alias").action(async (options)=>{
12
+ program.command("start").description("Start function emulator").option("-o, --output-dir <dir>", "Output directory").option("-P, --project <project id or alias>", "Project id or alias").option("--inspect-functions [port]", "emulate Cloud Functions in debug mode with the node inspector on the given port (9229 if not specified)").action(async (options)=>{
12
13
  try {
13
- const projectConfig = await resolveProjectConfig(options);
14
- await start(projectConfig);
14
+ const projectConfig = await resolveProjectConfig({
15
+ ...options,
16
+ isEmulator: true
17
+ });
18
+ await start(options.inspectFunctions ?? false, projectConfig);
15
19
  } catch (error) {
16
20
  console.error(error);
17
21
  process.exit(1);
18
22
  }
19
23
  });
20
24
  }
21
- async function start({ packageRoot, packageJSON, projectAlias, projectId, projectRoot, pnpmWorkspaceYAML, outputDir }) {
22
- const { name, version, exports: exports$1 } = await readPackageJSON(packageJSON);
25
+ async function start(inspectFunctions, projectConfig) {
26
+ const { packageRoot, packageJSON, projectId, projectRoot, pnpmWorkspaceYAML, outputDir } = projectConfig;
27
+ const { name, exports: exports$1 } = await readPackageJSON(packageJSON);
23
28
  const codebase = getPackageName(name);
24
29
  const packagesToInline = await getWorkspacePackageNames(pnpmWorkspaceYAML);
25
30
  const entryFile = new URL(exports$1?.["."] ?? "./index.js", packageRoot);
26
31
  console.log("🧹 Cleaning dist folder");
27
32
  await cleanDir(outputDir);
28
33
  console.log(`🔨 Building ${name}`);
29
- const bundleOutputDir = new URL("lib", outputDir);
30
34
  let firstRun = true;
31
35
  const onBundleEnd = async ({ output })=>{
32
- const main = `./lib/${output[0].fileName}`;
33
- const dependencies = await flattenDependencies(harmonizeDependencies(await calculateDependencies(output)));
34
- // const { contentHash } = await pack(functionGroupDir, distDir)
36
+ await writeDependencies(output, createOutputFileName(packageRoot, projectRoot), outputDir);
35
37
  const firebaseJSON = await getFirebaseJSON(new URL("firebase.json", projectRoot));
36
- await prepareStart(codebase, projectAlias, projectId, {
37
- name,
38
- type: "module",
39
- version,
40
- main,
41
- ...dependencies
42
- }, firebaseJSON, outputDir);
38
+ await prepareStart(codebase, firebaseJSON, projectConfig);
43
39
  if (firstRun) {
44
40
  firstRun = false;
45
- runEmulator(codebase, projectAlias, projectId, outputDir, []).catch((error)=>{
41
+ startEmulator({
42
+ projectId,
43
+ cwd: outputDir,
44
+ inspectFunctions
45
+ }).catch((error)=>{
46
46
  console.error(error);
47
- process.exit(1);
48
47
  });
49
48
  }
50
49
  };
51
- await bundleAndWatch(entryFile, bundleOutputDir, {
50
+ await bundleAndWatch(entryFile, {
51
+ outputDir,
52
+ projectRoot,
53
+ packageRoot,
52
54
  packagesToInline,
53
55
  onBundleEnd
54
56
  });
55
57
  }
56
- async function prepareStart(codebase, projectAlias, projectId, packageJSON, firebaseJSON, outputDir) {
58
+ async function prepareStart(codebase, firebaseJSON, { projectAlias, projectId, outputDir, envFiles }) {
59
+ const envPrefix = `FUNCTION_CODEBASE='${codebase}'\nENTUR_PROJECT_ALIAS='${projectAlias}'`;
57
60
  await Promise.all([
58
61
  writeFirebaseJSON(codebase, firebaseJSON, outputDir),
59
62
  createFirebaseRC(projectAlias, projectId, outputDir),
60
- writePackageJSON(new URL("./package.json", outputDir), packageJSON)
63
+ copyEnvFiles(envFiles, envPrefix, outputDir)
61
64
  ]);
62
65
  await linkDependencies(outputDir);
63
66
  }
@@ -68,23 +71,5 @@ async function createFirebaseRC(projectAlias, projectId, outputDir) {
68
71
  }
69
72
  });
70
73
  }
71
- function runEmulator(codebase, projectAlias, projectId, workingDir, extraArgs) {
72
- return spawnAsync("firebase", [
73
- "emulators:start",
74
- "--only",
75
- "functions",
76
- "-P",
77
- projectId,
78
- ...extraArgs
79
- ], {
80
- cwd: workingDir,
81
- env: {
82
- ...process.env,
83
- FUNCTION_CODEBASE: codebase,
84
- ENTUR_PROJECT_ALIAS: projectAlias,
85
- NODE_OPTIONS: process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} --enable-source-maps` : "--enable-source-maps"
86
- }
87
- });
88
- }
89
74
 
90
75
  export { registerStart };
@@ -2,6 +2,7 @@ type Options = {
2
2
  outputDir?: string;
3
3
  project?: string;
4
4
  cwd?: string;
5
+ isEmulator?: boolean;
5
6
  };
6
7
  type ProjectConfig = {
7
8
  pnpmWorkspaceYAML: URL;
@@ -11,8 +12,10 @@ type ProjectConfig = {
11
12
  projectRoot: URL;
12
13
  projectAlias: string;
13
14
  projectId: string;
15
+ envFiles: URL[];
14
16
  };
15
- declare function resolveProjectConfig({ outputDir, project, cwd, }?: Options): Promise<ProjectConfig>;
17
+ declare function resolveProjectConfig({ outputDir, project, isEmulator, cwd, }?: Options): Promise<ProjectConfig>;
18
+ declare function copyEnvFiles([envFile, ...envFiles]: URL[], prefix: string, outputDir: URL): Promise<void>;
16
19
 
17
- export { resolveProjectConfig };
20
+ export { copyEnvFiles, resolveProjectConfig };
18
21
  export type { ProjectConfig };
@@ -1,8 +1,12 @@
1
- import { pathToFileURL } from 'node:url';
1
+ import { readFile, writeFile, cp } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { pathToFileURL, fileURLToPath } from 'node:url';
2
4
  import { findUp } from 'find-up-simple';
3
- import { readJSON } from '../utils/fs.js';
5
+ import { asyncFilterMap } from '../utils/async.js';
6
+ import { readJSON, exists } from '../utils/fs.js';
4
7
 
5
- async function resolveProjectConfig({ outputDir = "dist", project, cwd = process.cwd() } = {}) {
8
+ const FUNCTIONS_EMULATOR_DOTENV = ".env.local";
9
+ async function resolveProjectConfig({ outputDir = "dist", project, isEmulator, cwd = process.cwd() } = {}) {
6
10
  const [pnpmWorkspacePath, packageJSONPath, firebaseRCPath] = await Promise.all([
7
11
  findUp("pnpm-workspace.yaml", {
8
12
  cwd
@@ -28,14 +32,18 @@ async function resolveProjectConfig({ outputDir = "dist", project, cwd = process
28
32
  const outputDirUrl = new URL(`./${outputDir}/`, `${pathToFileURL(cwd)}/`);
29
33
  const pnpmWorkspaceYAML = pathToFileURL(pnpmWorkspacePath);
30
34
  const packageJSONUrl = pathToFileURL(packageJSONPath);
35
+ const packageRoot = new URL("./", packageJSONUrl);
36
+ const projectRoot = new URL("./", pnpmWorkspaceYAML);
37
+ const envFiles = await findEnvFiles(packageRoot, projectId, projectAlias, isEmulator);
31
38
  return {
32
39
  pnpmWorkspaceYAML,
33
40
  packageJSON: packageJSONUrl,
34
41
  outputDir: outputDirUrl,
35
- packageRoot: new URL("./", packageJSONUrl),
36
- projectRoot: new URL("./", pnpmWorkspaceYAML),
42
+ packageRoot,
43
+ projectRoot,
37
44
  projectAlias,
38
- projectId
45
+ projectId,
46
+ envFiles
39
47
  };
40
48
  }
41
49
  function getProjectAliasAndId({ default: defaultProjectId, ...projects }, project) {
@@ -67,5 +75,34 @@ function getProjectAliasAndId({ default: defaultProjectId, ...projects }, projec
67
75
  }
68
76
  throw new Error(`No project with alias or id ${project} found in .firebaserc`);
69
77
  }
78
+ function findEnvFiles(packageRoot, projectId, projectAlias, isEmulator) {
79
+ const files = [
80
+ ".env"
81
+ ];
82
+ files.push(`.env.${projectId}`);
83
+ if (projectAlias) {
84
+ files.push(`.env.${projectAlias}`);
85
+ }
86
+ if (isEmulator) {
87
+ files.push(FUNCTIONS_EMULATOR_DOTENV);
88
+ }
89
+ return asyncFilterMap(files, async (fileName)=>{
90
+ const fileUrl = new URL(`./${fileName}`, packageRoot);
91
+ return await exists(fileUrl) ? fileUrl : undefined;
92
+ });
93
+ }
94
+ async function copyEnvFiles([envFile, ...envFiles], prefix, outputDir) {
95
+ if (envFile) {
96
+ const fileContent = await readFile(envFile, "utf8");
97
+ const fileName = basename(fileURLToPath(envFile));
98
+ await writeFile(new URL(`./${fileName}`, outputDir), `${prefix}\n${fileContent}`);
99
+ } else {
100
+ await writeFile(new URL("./.env", outputDir), prefix);
101
+ }
102
+ for (const envFile of envFiles){
103
+ const fileName = basename(fileURLToPath(envFile));
104
+ await cp(envFile, new URL(`./${fileName}`, outputDir));
105
+ }
106
+ }
70
107
 
71
- export { resolveProjectConfig };
108
+ export { copyEnvFiles, resolveProjectConfig };
@@ -0,0 +1,47 @@
1
+ import { spawnAsync } from '../utils/exec.js';
2
+
3
+ const FUNCTIONS_DISCOVERY_TIMEOUT = "30";
4
+ function deploy({ projectId, cwd }) {
5
+ return spawnAsync("firebase", [
6
+ "deploy",
7
+ "--only",
8
+ "functions,firestore",
9
+ "-P",
10
+ projectId
11
+ ], {
12
+ cwd,
13
+ env: {
14
+ ...process.env,
15
+ FUNCTIONS_DISCOVERY_TIMEOUT
16
+ }
17
+ });
18
+ }
19
+ function startEmulator({ projectId, cwd, inspectFunctions }) {
20
+ let extraArgs = [];
21
+ if (typeof inspectFunctions === "string") {
22
+ extraArgs = [
23
+ "--inspect-functions",
24
+ inspectFunctions
25
+ ];
26
+ } else if (inspectFunctions) {
27
+ extraArgs = [
28
+ "--inspect-functions"
29
+ ];
30
+ }
31
+ return spawnAsync("firebase", [
32
+ "emulators:start",
33
+ "--only",
34
+ "functions",
35
+ "-P",
36
+ projectId,
37
+ ...extraArgs
38
+ ], {
39
+ cwd,
40
+ env: {
41
+ ...process.env,
42
+ FUNCTIONS_DISCOVERY_TIMEOUT
43
+ }
44
+ });
45
+ }
46
+
47
+ export { deploy, startEmulator };
package/lib/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { ProjectConfig, resolveProjectConfig } from './commands/utils.js';
1
+ export { ProjectConfig, copyEnvFiles, resolveProjectConfig } from './commands/utils.js';
2
2
  export { Export, getExports } from './importExports/exports.js';
3
3
  export { Import, getImports } from './importExports/imports.js';
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { resolveProjectConfig } from './commands/utils.js';
1
+ export { copyEnvFiles, resolveProjectConfig } from './commands/utils.js';
2
2
  export { getExports } from './importExports/exports.js';
3
3
  export { getImports } from './importExports/imports.js';
@@ -12,20 +12,6 @@ function filterMap(arr, mapper) {
12
12
  return acc;
13
13
  }, []);
14
14
  }
15
- function groupBy(array, iteratee) {
16
- return array.reduce((map, item, index, array_)=>{
17
- const key = iteratee(item, index, array_);
18
- const existingGroup = map.get(key);
19
- if (existingGroup) {
20
- existingGroup.push(item);
21
- } else {
22
- map.set(key, [
23
- item
24
- ]);
25
- }
26
- return map;
27
- }, new Map());
28
- }
29
15
  function partition(array, predicate) {
30
16
  return array.reduce((result, value, index, arr)=>{
31
17
  const [include, exclude] = result;
@@ -41,4 +27,4 @@ function partition(array, predicate) {
41
27
  ]);
42
28
  }
43
29
 
44
- export { filterMap, groupBy, partition, uniq };
30
+ export { filterMap, partition, uniq };
package/lib/utils/fs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { rm, mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { rm, mkdir, readFile, writeFile, access } from 'node:fs/promises';
2
2
 
3
3
  async function cleanDir(dir) {
4
4
  await rm(dir, {
@@ -9,6 +9,14 @@ async function cleanDir(dir) {
9
9
  recursive: true
10
10
  });
11
11
  }
12
+ async function exists(fileOrDirectory) {
13
+ try {
14
+ await access(fileOrDirectory);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
12
20
  async function readJSON(file) {
13
21
  const fileContent = await readFile(file, "utf-8");
14
22
  return JSON.parse(fileContent);
@@ -17,4 +25,4 @@ async function writeJSON(file, data) {
17
25
  await writeFile(file, JSON.stringify(data, undefined, 4));
18
26
  }
19
27
 
20
- export { cleanDir, readJSON, writeJSON };
28
+ export { cleanDir, exists, readJSON, writeJSON };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@entur/function-tools",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -21,7 +21,6 @@
21
21
  "oxc-resolver": "^11.16.2",
22
22
  "rollup": "^4.54.0",
23
23
  "rollup-plugin-dts": "^6.3.0",
24
- "semver": "^7.7.3",
25
24
  "yaml": "^2.8.2"
26
25
  },
27
26
  "peerDependencies": {
@@ -1,159 +0,0 @@
1
- import { builtinModules } from 'node:module';
2
- import { dirname } from 'node:path';
3
- import { coerce, satisfies } from 'semver';
4
- import { groupBy } from './array.js';
5
- import { groupByAsync, asyncMap } from './async.js';
6
- import { spawnAsync } from './exec.js';
7
- import { readPackageJSON, packageUp } from './packageJSON.js';
8
-
9
- const alwaysIncludePackageNames = [
10
- "@google-cloud/functions-framework"
11
- ];
12
- async function calculateDependencies(outputs) {
13
- const outputGroupedByPackage = await groupByAsync(outputs.filter((it)=>Boolean(it.type === "chunk" && it.facadeModuleId && it.imports.length > 0)), async (output)=>{
14
- const key = output.facadeModuleId && await packageUp({
15
- cwd: output.facadeModuleId
16
- });
17
- if (!key) {
18
- throw new Error(`No package.json found for ${output.facadeModuleId}`);
19
- }
20
- return key;
21
- });
22
- const filterDependencies = (lookup, dependencies)=>{
23
- if (!dependencies) return;
24
- const entries = Object.entries(dependencies).filter(([key])=>lookup.has(key));
25
- return Object.fromEntries(entries);
26
- };
27
- const entries = await asyncMap([
28
- ...outputGroupedByPackage
29
- ], async ([packageJSONPath, outputs])=>{
30
- const packageJSON = await readPackageJSON(packageJSONPath);
31
- const importedPackageNames = new Set(outputs.flatMap((it)=>it.imports).filter((it)=>!isBuiltinImport(it)).map(specifierToPackageName));
32
- if (alwaysIncludePackageNames?.length) {
33
- alwaysIncludePackageNames.forEach((it)=>{
34
- importedPackageNames.add(it);
35
- });
36
- }
37
- return [
38
- dirname(packageJSONPath),
39
- {
40
- ...packageJSON,
41
- dependencies: filterDependencies(importedPackageNames, packageJSON.dependencies),
42
- devDependencies: filterDependencies(importedPackageNames, packageJSON.devDependencies),
43
- optionalDependencies: filterDependencies(importedPackageNames, packageJSON.optionalDependencies)
44
- }
45
- ];
46
- });
47
- return new Map(entries);
48
- }
49
- function specifierToPackageName(specifier) {
50
- if (specifier.startsWith("@")) {
51
- return specifier.split("/", 2).join("/");
52
- }
53
- return specifier.split("/", 1).join("/");
54
- }
55
- function isBuiltinImport(specifier) {
56
- return builtinModules.includes(specifier) || specifier.startsWith("node:");
57
- }
58
- function harmonizeDependencies(initialDependecies) {
59
- const entries = [
60
- ...initialDependecies.values()
61
- ].flatMap((it)=>[
62
- ...Object.entries(it.dependencies ?? {}),
63
- ...Object.entries(it.devDependencies ?? {}),
64
- ...Object.entries(it.peerDependencies ?? {}),
65
- ...Object.entries(it.optionalDependencies ?? {})
66
- ]);
67
- const harmonizedSemvers = new Map([
68
- ...groupBy(entries, ([packageName])=>packageName)
69
- ].map(([packageName, entriesForPackageName])=>{
70
- const firstVersion = entriesForPackageName[0][1];
71
- if (entriesForPackageName.length === 1) {
72
- return [
73
- packageName,
74
- firstVersion
75
- ];
76
- }
77
- if (entriesForPackageName.every(([, semver])=>semver === firstVersion)) {
78
- return [
79
- packageName,
80
- firstVersion
81
- ];
82
- }
83
- const satisfiedVersion = entriesForPackageName.reduce((acc, [, semver])=>{
84
- if (acc === semver) {
85
- return acc;
86
- }
87
- const semVerOrNull = coerce(semver);
88
- if (semVerOrNull !== null && satisfies(semVerOrNull, acc)) {
89
- console.log(`${packageName} ${semver} over ${acc}`);
90
- return semver;
91
- }
92
- if (semVerOrNull !== null && satisfies(semVerOrNull, semver)) {
93
- console.log(`${packageName} ${acc} over ${semver}`);
94
- return acc;
95
- }
96
- throw new Error(`Unable to find determine correct version of ${packageName} between ${semver} and ${acc}`);
97
- }, firstVersion);
98
- return [
99
- packageName,
100
- satisfiedVersion
101
- ];
102
- }));
103
- const harmonizeSemver = (dependencies)=>{
104
- if (!dependencies) return;
105
- const entries = Object.entries(dependencies).map(([packageName, initialSemver])=>{
106
- const semver = harmonizedSemvers.get(packageName);
107
- return [
108
- packageName,
109
- semver ?? initialSemver
110
- ];
111
- });
112
- return Object.fromEntries(entries);
113
- };
114
- return new Map([
115
- ...initialDependecies
116
- ].map(([packageName, packageJSON])=>{
117
- return [
118
- packageName,
119
- {
120
- ...packageJSON,
121
- dependencies: harmonizeSemver(packageJSON.dependencies),
122
- devDependencies: harmonizeSemver(packageJSON.devDependencies),
123
- peerDependencies: harmonizeSemver(packageJSON.peerDependencies)
124
- }
125
- ];
126
- }));
127
- }
128
- // optionalDependencies: harmonizeSemver(packageJSON.optionalDependencies),
129
- async function flattenDependencies(dependencies) {
130
- const listOfDependencies = [
131
- ...dependencies.values()
132
- ].map((it)=>it.dependencies);
133
- const listOfDevDependencies = [
134
- ...dependencies.values()
135
- ].map((it)=>it.devDependencies);
136
- const listOfPeerDependencies = [
137
- ...dependencies.values()
138
- ].map((it)=>it.peerDependencies);
139
- const listOfOptionalDependencies = [
140
- ...dependencies.values()
141
- ].map((it)=>it.optionalDependencies);
142
- return {
143
- dependencies: Object.assign({}, ...listOfDependencies),
144
- devDependencies: Object.assign({}, ...listOfDevDependencies),
145
- peerDependencies: Object.assign({}, ...listOfPeerDependencies),
146
- optionalDependencies: Object.assign({}, ...listOfOptionalDependencies)
147
- };
148
- }
149
- async function linkDependencies(workingDir) {
150
- await spawnAsync("pnpm", [
151
- "install",
152
- "--ignore-workspace",
153
- "--prefer-offline"
154
- ], {
155
- cwd: workingDir
156
- });
157
- }
158
-
159
- export { calculateDependencies, flattenDependencies, harmonizeDependencies, linkDependencies };