From 23597c378961d4cc7ded2933cdfb414835ed390d Mon Sep 17 00:00:00 2001 From: Vlad Sirenko Date: Fri, 17 Nov 2023 16:30:52 -0800 Subject: [PATCH] add parsing env file --- README.md | 16 +++++++++ schema.json | 7 ++++ src/analyzer.ts | 79 ++++++++++++++++++++++++++++++++++++++++---- src/read-env-file.ts | 33 ++++++++++++++++++ 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/read-env-file.ts diff --git a/README.md b/README.md index 1695c692e..39f4f600e 100644 --- a/README.md +++ b/README.md @@ -484,6 +484,22 @@ should not be re-used. } ``` +### Parsing the env file + +```json +{ + "wireit": { + "my-script": { + "command": "my-command", + "env-file": [ + ".env", + ".dev.env" + ] + } + } +} +``` + ## Services By default, Wireit assumes that your scripts will eventually exit by themselves. diff --git a/schema.json b/schema.json index 8369a8d40..7be60c286 100644 --- a/schema.json +++ b/schema.json @@ -117,6 +117,13 @@ } ] } + }, + "env-file": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } } }, "type": "object" diff --git a/src/analyzer.ts b/src/analyzer.ts index d7c679aaa..2f6b213d6 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -30,6 +30,7 @@ import type { } from './config.js'; import type {Agent} from './cli-options.js'; import {Logger} from './logging/logger.js'; +import {readEnvFile} from './read-env-file.js'; export interface AnalyzeResult { config: Result; @@ -630,7 +631,12 @@ export class Analyzer { files, ); - const env = this.#processEnv(placeholder, packageJson, syntaxInfo, command); + const entries = this.#processEnv(placeholder, packageJson, syntaxInfo, command); + await this.#processEnvFile(placeholder, packageJson, syntaxInfo, command, entries); + + // Sort for better fingerprint match rate. + entries.sort(); + const env = Object.fromEntries(entries); if (placeholder.failures.length > 0) { // A script with locally-determined errors doesn't get upgraded to @@ -1319,13 +1325,13 @@ export class Analyzer { packageJson: PackageJson, syntaxInfo: ScriptSyntaxInfo, command: JsonAstNode | undefined, - ): Record { + ): Array<[string, string]> { if (syntaxInfo.wireitConfigNode === undefined) { - return {}; + return []; } const envNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, ['env']); if (envNode === undefined) { - return {}; + return []; } if (command === undefined) { placeholder.failures.push({ @@ -1358,7 +1364,7 @@ export class Analyzer { }); } if (envNode.children === undefined) { - return {}; + return []; } const entries: Array<[string, string]> = []; for (const propNode of envNode.children) { @@ -1428,8 +1434,67 @@ export class Analyzer { } } // Sort for better fingerprint match rate. - entries.sort(); - return Object.fromEntries(entries); + return entries; + } + + async #processEnvFile( + placeholder: UnvalidatedConfig, + packageJson: PackageJson, + syntaxInfo: ScriptSyntaxInfo, + command: JsonAstNode | undefined, + entries: Array<[string, string]> + ): Promise { + if (syntaxInfo.wireitConfigNode === undefined) { + return; + } + const envFileNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, ['env-file']); + if (envFileNode === undefined) { + return; + } + if (command === undefined) { + placeholder.failures.push({ + type: 'failure', + reason: 'invalid-config-syntax', + script: placeholder, + diagnostic: { + severity: 'error', + message: 'Can\'t set "env-file" unless "command" is set', + location: { + file: packageJson.jsonFile, + range: {length: envFileNode.length, offset: envFileNode.offset}, + }, + }, + }); + } + if (envFileNode.type !== 'array') { + placeholder.failures.push({ + type: 'failure', + reason: 'invalid-config-syntax', + script: placeholder, + diagnostic: { + severity: 'error', + message: 'Expected an array', + location: { + file: packageJson.jsonFile, + range: {length: envFileNode.length, offset: envFileNode.offset}, + }, + }, + }); + } + if (envFileNode.children === undefined) { + return; + } + const promiseEnvFiles: Array> = []; + for (const propNode of envFileNode.children) { + if (propNode.type !== 'string') { + throw new Error( + 'Internal error: expected array JSON node child to be string', + ); + } + const envFile = propNode.value as string; + promiseEnvFiles.push(readEnvFile(envFile, entries)); + } + await Promise.all(promiseEnvFiles); } /** diff --git a/src/read-env-file.ts b/src/read-env-file.ts new file mode 100644 index 000000000..2b4c03479 --- /dev/null +++ b/src/read-env-file.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {readFile, stat} from 'node:fs/promises'; + +// TODO string-template-parser + +export async function readEnvFile(filepath: string, entries: Array<[string, string]>): Promise { + const fileStat = await stat(filepath); + if (!fileStat.isFile()) { + console.warn('Skipping non-file env file: ' + filepath); + return; + } + const content = await readFile(filepath, 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('#')) { + continue; + } + const [key, ...rest] = trimmed.split('='); + if (rest.length === 0) { + continue; + } + const value = rest.join('='); + if (typeof key === 'string' && typeof value === 'string') { + entries.push([key, value]); + } + } +}