edge-functions 1.3.0 → 1.5.0

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.
Files changed (34) hide show
  1. package/.eslintrc.json +4 -0
  2. package/aliases.js +1 -0
  3. package/examples/vue-static/yarn.lock +13094 -0
  4. package/examples/vue-vite-static/yarn.lock +333 -0
  5. package/jsconfig.json +3 -0
  6. package/lib/build/dispatcher/dispatcher.js +30 -21
  7. package/lib/commands/auth.commands.js +85 -0
  8. package/lib/commands/build.commands.js +47 -0
  9. package/lib/commands/deploy.commands.js +45 -0
  10. package/lib/commands/dev.commands.js +26 -0
  11. package/lib/commands/index.js +13 -0
  12. package/lib/commands/init.commands.js +61 -0
  13. package/lib/commands/logs.commands.js +34 -0
  14. package/lib/commands/presets.commands.js +97 -0
  15. package/lib/commands/storage.commands.js +22 -0
  16. package/lib/constants/framework-initializer.constants.js +2 -2
  17. package/lib/constants/messages/build.messages.js +1 -0
  18. package/lib/env/polyfills/fetch.polyfills.js +10 -10
  19. package/lib/env/runtime.env.js +1 -1
  20. package/lib/env/server.env.js +101 -23
  21. package/lib/env/vulcan.env.js +43 -21
  22. package/lib/main.js +36 -261
  23. package/lib/platform/actions/core/auth.actions.js +3 -3
  24. package/lib/platform/services/base.service.js +1 -2
  25. package/lib/presets/custom/next/deliver/prebuild.js +4 -1
  26. package/lib/presets/custom/vue/deliver/prebuild.js +2 -5
  27. package/lib/utils/getUrlFromResource/getUrlFromResource.utils.js +16 -0
  28. package/lib/utils/getUrlFromResource/index.js +3 -0
  29. package/lib/utils/index.js +2 -0
  30. package/package.json +4 -3
  31. package/releaserc.json +1 -0
  32. package/lib/polyfills/FetchEvent.polyfills.js +0 -13
  33. package/lib/polyfills/fetch.polyfills.js +0 -39
  34. package/lib/polyfills/index.js +0 -4
@@ -0,0 +1,13 @@
1
+ import authCommand from './auth.commands.js';
2
+ import buildCommand from './build.commands.js';
3
+ import deployCommand from './deploy.commands.js';
4
+ import devCommand from './dev.commands.js';
5
+ import initCommand from './init.commands.js';
6
+ import logsCommand from './logs.commands.js';
7
+ import presetsCommand from './presets.commands.js';
8
+ import storageCommand from './storage.commands.js';
9
+
10
+ export {
11
+ authCommand, buildCommand, deployCommand, devCommand,
12
+ initCommand, logsCommand, presetsCommand, storageCommand,
13
+ };
@@ -0,0 +1,61 @@
1
+ import { createPromptModule } from 'inquirer';
2
+ import { FrameworkInitializer, Messages } from '#constants';
3
+ import { feedback } from '#utils';
4
+ import { vulcan } from '#env';
5
+
6
+ const prompt = createPromptModule();
7
+ /**
8
+ * A command to Initializes a new project with the selected framework template.
9
+ * @memberof commands
10
+ * This function prompts the user to select a framework template and enter a project name.
11
+ * Then it initializes a new project based on the selected template.
12
+ * @param {object} options - An object containing the name for the new project.
13
+ * @param {string} options.name - The name of the new project.
14
+ * If not provided, the function will prompt for it.
15
+ * @returns {Promise<void>} - A promise that resolves when the new project is initialized.
16
+ * @example
17
+ *
18
+ * initCommand({ name: 'my_new_project' });
19
+ */
20
+ async function initComamnd({ name }) {
21
+ const AVALIABLE_TEMPLATES = Object.keys(FrameworkInitializer);
22
+ let projectName = name;
23
+
24
+ const { frameworkChoice } = await prompt([
25
+ {
26
+ type: 'list',
27
+ name: 'frameworkChoice',
28
+ message: 'Choose a template for your project:',
29
+ choices: AVALIABLE_TEMPLATES,
30
+ },
31
+ ]);
32
+
33
+ while (!projectName) {
34
+ // eslint-disable-next-line no-await-in-loop
35
+ const { projectName: inputName } = await prompt([
36
+ {
37
+ type: 'input',
38
+ name: 'projectName',
39
+ message: 'Enter your project name:',
40
+ },
41
+ ]);
42
+
43
+ if (inputName) {
44
+ projectName = inputName;
45
+ }
46
+ if (!inputName) {
47
+ feedback.pending(Messages.info.name_required);
48
+ }
49
+ }
50
+
51
+ const createFrameworkTemplate = FrameworkInitializer[frameworkChoice];
52
+
53
+ if (createFrameworkTemplate) {
54
+ process.env.VULCAN_CURRENT_PRESET = projectName; // for Azion CLI in Golang(temp)
55
+ await createFrameworkTemplate(projectName);
56
+ } else {
57
+ feedback.error(Messages.errors.invalid_choice);
58
+ }
59
+ }
60
+
61
+ export default initComamnd;
@@ -0,0 +1,34 @@
1
+ import { feedback } from '#utils';
2
+ import { Messages } from '#constants';
3
+ /**
4
+ * A comamnd to display logs for a specified function or application.
5
+ * @memberof commands
6
+ * This function allows the user to view logs for a specific function or application.
7
+ * Note that viewing logs for applications is currently unsupported.
8
+ * @param {string} type - The type of entity to show logs for.
9
+ * Accepted values are 'function' and 'application'.
10
+ * @param {string} id - The identifier for the function or application.
11
+ * @param {object} options - Additional options for fetching logs.
12
+ * @param {boolean} options.watch - If true, keep watching the logs and updating in real-time.
13
+ * @returns {Promise<void>} - A promise that resolves when logs are fetched and displayed.
14
+ * @example
15
+ *
16
+ * logsCommand('function', 'functionId123', { watch: true });
17
+ */
18
+ async function logsCommand(type, id, { watch }) {
19
+ const { functions } = await import('#platform');
20
+
21
+ if (!['function', 'application'].includes(type)) {
22
+ feedback.error(Messages.platform.logs.errors.invalid_log_type);
23
+ return;
24
+ }
25
+
26
+ if (type === 'function') {
27
+ functions.actions.showFunctionLogs(id, watch);
28
+ }
29
+ if (type === 'application') {
30
+ feedback.info(Messages.platform.logs.info.unsupported_log_type);
31
+ }
32
+ }
33
+
34
+ export default logsCommand;
@@ -0,0 +1,97 @@
1
+ import { createPromptModule } from 'inquirer';
2
+ import { Messages } from '#constants';
3
+ import { feedback, debug } from '#utils';
4
+
5
+ const prompt = createPromptModule();
6
+
7
+ /**
8
+ * Manages presets for the application.
9
+ * @memberof commands
10
+ * This command allows the user to create or list presets.
11
+ * The user is guided by a series of prompts to enter a preset name and mode.
12
+ * @param {string} command - The operation to be performed:
13
+ * 'create' to create a preset, 'ls' to list presets.
14
+ * @returns {Promise<void>} - A promise that resolves when the action is complete.
15
+ * @example
16
+ *
17
+ * // To create a new preset
18
+ * presetsCommand('create');
19
+ *
20
+ * // To list existing presets
21
+ * presetsCommand('ls');
22
+ */
23
+ async function presetsCommand(command) {
24
+ const { presets } = await import('#utils');
25
+
26
+ let name;
27
+ let mode;
28
+
29
+ switch (command) {
30
+ case 'create':
31
+ // eslint-disable-next-line no-constant-condition
32
+ while (true) {
33
+ // eslint-disable-next-line no-await-in-loop
34
+ const { inputPresetName } = await prompt([
35
+ {
36
+ type: 'input',
37
+ name: 'inputPresetName',
38
+ message: 'Enter the preset name:',
39
+ },
40
+ ]);
41
+
42
+ const presetExists = presets
43
+ .getKeys()
44
+ .map((existingPresetName) => existingPresetName.toLowerCase())
45
+ .includes(inputPresetName.toLowerCase());
46
+
47
+ if (presetExists) {
48
+ feedback.error('A preset with this name already exists.');
49
+ } else if (!inputPresetName) {
50
+ feedback.error('Preset name cannot be empty.');
51
+ } else {
52
+ name = inputPresetName;
53
+ break;
54
+ }
55
+ }
56
+
57
+ // eslint-disable-next-line no-constant-condition
58
+ while (true) {
59
+ // eslint-disable-next-line no-await-in-loop
60
+ const { inputMode } = await prompt([
61
+ {
62
+ type: 'list',
63
+ name: 'inputMode',
64
+ message: 'Choose the mode:',
65
+ choices: ['compute', 'deliver'],
66
+ },
67
+ ]);
68
+
69
+ if (['compute', 'deliver'].includes(inputMode)) {
70
+ mode = inputMode;
71
+ break;
72
+ } else {
73
+ feedback.error('Invalid mode. Choose either "compute" or "deliver".');
74
+ }
75
+ }
76
+
77
+ try {
78
+ presets.set(name, mode);
79
+ feedback.success(`${name}(${mode}) created with success!`);
80
+ feedback.info(`Now open './lib/presets/${name}/${mode}' and work on your preset.`);
81
+ } catch (error) {
82
+ debug.error(error);
83
+ feedback.error(Messages.errors.folder_creation_failed(name));
84
+ }
85
+ break;
86
+
87
+ case 'ls':
88
+ presets.getBeautify().forEach((preset) => feedback.option(preset));
89
+ break;
90
+
91
+ default:
92
+ feedback.error('Invalid argument provided.');
93
+ break;
94
+ }
95
+ }
96
+
97
+ export default presetsCommand;
@@ -0,0 +1,22 @@
1
+ import { join } from 'path';
2
+
3
+ /**
4
+ * A command to uploads static files for a given version of the application.
5
+ * @memberof commands
6
+ * The function identifies the build version, prepares a base path for static files,
7
+ * and uploads these files to the storage of the core platform.
8
+ * @returns {Promise<void>} - A promise that resolves when static files are successfully uploaded.
9
+ * @example
10
+ *
11
+ * storageCommand();
12
+ */
13
+ async function storageCommand() {
14
+ const { core } = await import('#platform');
15
+ const { getVulcanBuildId } = await import('#utils');
16
+
17
+ const versionId = getVulcanBuildId();
18
+ const basePath = join(process.cwd(), '.edge/storage/');
19
+ await core.actions.uploadStatics(versionId, basePath);
20
+ }
21
+
22
+ export default storageCommand;
@@ -26,7 +26,7 @@ import { exec } from '#utils';
26
26
 
27
27
  const FrameworkInitializer = {
28
28
  Angular: async (projectName) => {
29
- await exec(`npx ng new ${projectName}`, 'Angular', false, true);
29
+ await exec(`npx @angular/cli new ${projectName}`, 'Angular', false, true);
30
30
  },
31
31
  Astro: async (projectName) => {
32
32
  await exec(`npx create-astro ${projectName}`, 'Astro', false, true);
@@ -41,7 +41,7 @@ const FrameworkInitializer = {
41
41
  await exec(`npx create-react-app ${projectName}`, 'React', false, true);
42
42
  },
43
43
  Vue: async (projectName) => {
44
- await exec(`npx vue create ${projectName}`, 'Vue', false, true);
44
+ await exec(`npx @vue/cli create ${projectName}`, 'Vue', false, true);
45
45
  },
46
46
  Vite: async (projectName) => {
47
47
  await exec(`npx create-vue ${projectName}`, 'Vue/Vite', false, true);
@@ -10,6 +10,7 @@ const build = {
10
10
  vulcan_build_succeeded: 'Vulcan Build succeeded!',
11
11
  },
12
12
  info: {
13
+ rebuilding: 'We are rebuilding with the new changes...',
13
14
  prebuild_starting: 'Starting prebuild...',
14
15
  vulcan_build_starting: 'Starting Vulcan build...',
15
16
 
@@ -2,6 +2,7 @@ import { join } from 'path';
2
2
  import { readFileSync } from 'fs';
3
3
  import mime from 'mime-types';
4
4
  import { EdgeRuntime } from 'edge-runtime';
5
+ import { getUrlFromResource, getVulcanBuildId } from '#utils';
5
6
 
6
7
  /**
7
8
  * A custom fetch implementation that adds an additional path to the URL if it starts with 'file://'.
@@ -9,19 +10,18 @@ import { EdgeRuntime } from 'edge-runtime';
9
10
  * it behaves as if the request is made from within the edge itself. In this case, an additional
10
11
  * '.edge/storage' folder is appended to the URL to represent the edge environment.
11
12
  * @param {EdgeRuntime} context - VMContext
12
- * @param {URL} url - The URL to fetch.
13
+ * @param {URL|Request|string} resource - The resource to fetch.
13
14
  * @param {object} [options] - The fetch options.
14
15
  * @returns {Promise<Response>} A Promise that resolves to the Response object.
15
16
  */
16
- async function fetchPolyfill(context, url, options) {
17
- const {
18
- URL, Headers, Response,
19
- } = context;
17
+ async function fetchPolyfill(context, resource, options) {
18
+ const { Headers, Response } = context;
20
19
 
21
- const urlOBJ = new URL(url);
22
- if (urlOBJ.href.startsWith('file://')) {
23
- // url pathname = /VERSION_ID/filePath
24
- const file = url.pathname.slice(15);
20
+ const urlObj = getUrlFromResource(resource);
21
+
22
+ if (urlObj.href.startsWith('file://')) {
23
+ const versionId = getVulcanBuildId();
24
+ const file = urlObj.pathname.replace(`/${versionId}`, '');
25
25
  const filePath = join(process.cwd(), '.edge', 'storage', file);
26
26
  const fileContent = readFileSync(filePath);
27
27
  const contentType = mime.lookup(filePath) || 'application/octet-stream';
@@ -33,7 +33,7 @@ async function fetchPolyfill(context, url, options) {
33
33
  return response;
34
34
  }
35
35
 
36
- return fetch(url, options);
36
+ return fetch(resource, options);
37
37
  }
38
38
 
39
39
  export default fetchPolyfill;
@@ -36,7 +36,7 @@ import { fetchPolyfill, FetchEventPolyfill } from './polyfills/index.js';
36
36
  */
37
37
  function runtime(code) {
38
38
  const extend = (context) => {
39
- context.fetch = (url, options) => fetchPolyfill(context, url, options);
39
+ context.fetch = (resource, options) => fetchPolyfill(context, resource, options);
40
40
  context.FetchEvent = FetchEventPolyfill;
41
41
  context.FirewallEvent = {}; // TODO: Firewall Event
42
42
  /*
@@ -1,46 +1,124 @@
1
- import { debug, readWorkerFile, feedback } from '#utils';
1
+ import {
2
+ debug, readWorkerFile, feedback, exec,
3
+ } from '#utils';
2
4
  import { Messages } from '#constants';
3
- import { runServer } from 'edge-runtime';
5
+ import edgeRuntimePackage from 'edge-runtime';
6
+ import chokidar from 'chokidar';
4
7
  import runtime from './runtime.env.js';
8
+ import vulcan from './vulcan.env.js';
9
+
10
+ const { runServer, EdgeRuntimeServer } = edgeRuntimePackage;
11
+
12
+ let currentServer;
13
+ let isChangeHandlerRunning = false;
5
14
 
6
15
  /**
7
- * Read the worker code from the file.
8
- * @param {string} workerPath - The path to the file containing the code.
9
- * @returns {Promise<string>} A promise that resolves to the worker code.
16
+ * Read the worker code from a specified path.
17
+ * @param {string} workerPath - Path to the worker file.
18
+ * @returns {Promise<string>} - The worker code.
19
+ * @throws {Error} - If unable to read the worker file.
10
20
  */
11
21
  async function readWorkerCode(workerPath) {
12
22
  try {
13
- const worker = await readWorkerFile(workerPath);
14
- return worker;
23
+ return await readWorkerFile(workerPath);
15
24
  } catch (error) {
16
25
  debug.error(error);
17
- feedback.server.error(
18
- Messages.env.server.errors.load_worker_failed(workerPath),
19
- );
26
+ feedback.server.error(Messages.env.server.errors.load_worker_failed(workerPath));
20
27
  throw error;
21
28
  }
22
29
  }
23
30
 
24
31
  /**
25
- * Start the HTTP server with the specified port and file path to load the code from.
26
- * @param {string} workerPath - The path to the file containing the worker code.
27
- * @param {number} port - The port to listen on.
32
+ * Initialize and run the server with the given port and worker code.
33
+ * @param {number} port - The port number.
34
+ * @param {string} workerCode - The worker code.
35
+ * @returns {Promise<EdgeRuntimeServer>} - The initialized server.
28
36
  */
29
- async function startServer(workerPath, port) {
37
+ async function initializeServer(port, workerCode) {
38
+ const execution = runtime(workerCode);
39
+ return runServer({ port, host: 'localhost', runtime: execution });
40
+ }
41
+
42
+ /**
43
+ * Handle server operations: start, restart.
44
+ * @param {string} workerPath - Path to the worker file.
45
+ * @param {number} port - The port number.
46
+ */
47
+ async function manageServer(workerPath, port) {
30
48
  try {
31
49
  const workerCode = await readWorkerCode(workerPath);
32
- const execution = runtime(workerCode);
33
- const server = await runServer({
34
- port,
35
- host: 'localhost',
36
- runtime: execution,
37
- });
38
- feedback.server.success(
39
- Messages.env.server.success.server_running(server.url),
40
- );
50
+
51
+ if (currentServer) {
52
+ await currentServer.close();
53
+ }
54
+
55
+ try {
56
+ currentServer = await initializeServer(port, workerCode);
57
+ feedback.server.success(Messages.env.server.success.server_running(`http://localhost:${port}`));
58
+ } catch (error) {
59
+ if (error.code === 'EADDRINUSE') {
60
+ await manageServer(workerPath, port + 1);
61
+ } else {
62
+ throw error;
63
+ }
64
+ }
41
65
  } catch (error) {
42
66
  debug.error(error);
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Handle file changes and prevent concurrent execution.
73
+ * @param {string} path - Path of the changed file.
74
+ * @param {string} workerPath - Path to the worker file.
75
+ * @param {number} port - The port number.
76
+ */
77
+ async function handleFileChange(path, workerPath, port) {
78
+ if (isChangeHandlerRunning) return;
79
+
80
+ isChangeHandlerRunning = true;
81
+
82
+ if (path.startsWith('vulcan') || path.startsWith('.edge') || path.startsWith('node_modules/.cache')) {
83
+ isChangeHandlerRunning = false;
84
+ return;
43
85
  }
86
+
87
+ const {
88
+ entry, preset, mode, useNodePolyfills,
89
+ } = await vulcan.readVulcanEnv('local');
90
+
91
+ feedback.build.info(Messages.build.info.rebuilding);
92
+
93
+ try {
94
+ await exec(`vulcan build --entry ${entry} --preset ${preset} --mode ${mode} --useNodePolyfills ${useNodePolyfills}`);
95
+ await manageServer(workerPath, port);
96
+ } catch (error) {
97
+ debug.error(`Build or server restart failed: ${error}`);
98
+ }
99
+
100
+ isChangeHandlerRunning = false;
101
+ }
102
+
103
+ /**
104
+ * Entry point function to start the server and watch for file changes.
105
+ * @param {string} workerPath - Path to the worker file.
106
+ * @param {number} port - The port number.
107
+ */
108
+ async function startServer(workerPath, port) {
109
+ await manageServer(workerPath, port); // Initialize the server for the first time
110
+
111
+ const watcher = chokidar.watch('./', {
112
+ persistent: true,
113
+ ignoreInitial: false,
114
+ depth: 99,
115
+ });
116
+
117
+ watcher.on('change', async (path) => {
118
+ await handleFileChange(path, workerPath, port);
119
+ });
120
+
121
+ watcher.on('error', (error) => debug.error(`Watcher error: ${error}`));
44
122
  }
45
123
 
46
124
  export default startServer;
@@ -4,17 +4,23 @@ import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
 
6
6
  /**
7
- * Create or update the Vulcan environment file with the specified name and value.
8
- * @param {string} name - The name of the variable.
9
- * @param {string} value - The value of the variable.
7
+ * Creates or updates Vulcan environment variables, either at the global or project level.
8
+ * @async
9
+ * @param {object} variables - An object containing the environment variables to set.
10
+ * @param {string} [scope='local'] - Determines the scope of the variable ('global' or 'local').
11
+ * @throws {Error} Throws an error if the environment file cannot be read or written.
12
+ * @example
13
+ * // Set multiple global environment variables
14
+ * createVulcanEnv({ API_KEY: 'abc123', ANOTHER_KEY: 'xyz' }, 'global')
15
+ * .catch(error => console.error(error));
10
16
  */
11
- async function createVulcanEnv(name, value) {
12
- const azionDirPath = path.join(process.env.HOME, '.azion');
13
- const vulcanEnvPath = path.join(azionDirPath, 'vulcan.env');
17
+ async function createVulcanEnv(variables, scope = 'global') {
18
+ const basePath = scope === 'global' ? path.join(process.env.HOME, '.azion') : path.join(process.cwd());
19
+ const vulcanEnvPath = path.join(basePath, 'vulcan.env');
14
20
 
15
21
  // Create the .azion folder if it doesn't exist
16
22
  try {
17
- await fs.mkdir(azionDirPath, { recursive: true });
23
+ await fs.mkdir(basePath, { recursive: true });
18
24
  } catch (error) {
19
25
  debug.error(error);
20
26
  feedback.error(Messages.errors.folder_creation_failed(vulcanEnvPath));
@@ -26,7 +32,6 @@ async function createVulcanEnv(name, value) {
26
32
  try {
27
33
  envData = await fs.readFile(vulcanEnvPath, 'utf8');
28
34
  } catch (error) {
29
- // Ignore error if the file doesn't exist
30
35
  if (error.code !== 'ENOENT') {
31
36
  debug.error(error);
32
37
  feedback.error(Messages.errors.file_doesnt_exist(vulcanEnvPath));
@@ -34,15 +39,17 @@ async function createVulcanEnv(name, value) {
34
39
  }
35
40
  }
36
41
 
37
- // Update or add the variable to the environment data
38
- const variableLine = `${name}=${value}`;
39
- const variableRegex = new RegExp(`${name}=.+`);
42
+ // Update or add each variable to the environment data
43
+ Object.entries(variables).forEach(([key, value]) => {
44
+ const variableLine = `${key}=${value}`;
45
+ const variableRegex = new RegExp(`${key}=.+`);
40
46
 
41
- if (envData.match(variableRegex)) {
42
- envData = envData.replace(variableRegex, variableLine);
43
- } else {
44
- envData += `${variableLine}\n`;
45
- }
47
+ if (envData.match(variableRegex)) {
48
+ envData = envData.replace(variableRegex, variableLine);
49
+ } else {
50
+ envData += `${variableLine}\n`;
51
+ }
52
+ });
46
53
 
47
54
  // Write the updated environment data to the file
48
55
  try {
@@ -55,12 +62,27 @@ async function createVulcanEnv(name, value) {
55
62
  }
56
63
 
57
64
  /**
58
- * Reads the vulcan.env file and returns an object with the variables and their values.
59
- * @returns {object|null} An object with the variables and their values,
60
- * or null if the file doesn't exist.
65
+ * Reads the vulcan.env file, either at the global or project level,
66
+ * and returns an object with the variables and their values.
67
+ * @param {string} [scope='local'] - Determines the scope of the environment
68
+ * file ('global' or 'local').
69
+ * @returns {Promise<object|null>} A promise that resolves to an object with
70
+ * the variables and their values, or null if the file doesn't exist.
71
+ * @throws {Error} Throws an error if the environment file cannot be read.
72
+ * @example
73
+ * // Read global environment variables
74
+ * readVulcanEnv('global')
75
+ * .then(env => console.log(env))
76
+ * .catch(error => console.error(error));
77
+ *
78
+ * // Read project-level environment variables
79
+ * readVulcanEnv('local')
80
+ * .then(env => console.log(env))
81
+ * .catch(error => console.error(error));
61
82
  */
62
- async function readVulcanEnv() {
63
- const vulcanEnvPath = path.join(process.env.HOME, '.azion', 'vulcan.env');
83
+ async function readVulcanEnv(scope = 'global') {
84
+ const basePath = scope === 'global' ? path.join(process.env.HOME, '.azion') : path.join(process.cwd());
85
+ const vulcanEnvPath = path.join(basePath, 'vulcan.env');
64
86
 
65
87
  try {
66
88
  // Check if the vulcan.env file exists