Skip to content

Commit

Permalink
Enable inline strings and env variables to be Sky API keys (#397)
Browse files Browse the repository at this point in the history
* Update the xstate sky cmd to use the api key from the file

* remove helper script

* Add changeset

* spelling 🤦‍♂️

* Make eval a bit safer

* Don’t use eval to extract API key

* Fix bug in getRootIdentifierOfDeepMemberExpression

* Remove Sky from the shared package

* Rename to StringLiteralOrEnvKey

* Use babel to get the identifier

* Try getting env variable from process before searching for files
  • Loading branch information
mellson authored Nov 14, 2023
1 parent d9cc11d commit 9789cd7
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-waves-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xstate/cli': patch
---

Make it possible to define the API keys for Sky inline using strings or Node-compatible environment variables like `process.env.SKY_API_KEY`.
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "esbuild src/bin.ts --outdir=bin --platform=node --format=cjs --bundle --loader:.node=file"
},
"devDependencies": {
"@statelyai/sky": "0.0.4",
"@statelyai/sky": "0.0.7",
"@types/isomorphic-fetch": "^0.0.36",
"@types/jest": "^27.4.0",
"@types/node": "^16.0.1",
Expand Down
16 changes: 13 additions & 3 deletions apps/cli/src/sky/writeConfigToFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {
modifySkyConfigSource,
skyConfigExtractFromFile,
} from '@xstate/machine-extractor';
import { doesSkyConfigExist, writeSkyConfig } from '@xstate/tools-shared';
import 'dotenv/config';
import * as fs from 'fs/promises';
import fetch from 'isomorphic-fetch';
import { writeToFiles } from '../typegen/writeToFiles';
import { getPrettierInstance } from '../utils';
import { fetchSkyConfig } from './urlUtils';
import {
doesSkyConfigExist,
writeSkyConfig,
} from './writeToFetchedMachineFile';

export const writeConfigToFiles = async (opts: {
uri: string;
Expand All @@ -23,8 +26,12 @@ export const writeConfigToFiles = async (opts: {
console.log(`${opts.uri} - skipping, sky config already exists`);
return;
}
const fileContents = await fs.readFile(opts.uri, 'utf8');
const parseResult = skyConfigExtractFromFile(fileContents);
const fileContent = await fs.readFile(opts.uri, 'utf8');
const parseResult = skyConfigExtractFromFile({
fileContent,
filePath: opts.uri,
cwd: opts.cwd,
});
if (!parseResult) return;
await Promise.all(
parseResult.skyConfigs.map(async (config) => {
Expand Down Expand Up @@ -54,6 +61,9 @@ export const writeConfigToFiles = async (opts: {
});
try {
const skyConfig = (await configResponse.json()) as SkyConfig;
if ('error' in skyConfig) {
throw new Error(skyConfig.error);
}
await writeSkyConfig({
filePath: opts.uri,
skyConfig,
Expand Down
2 changes: 1 addition & 1 deletion packages/machine-extractor/src/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export const objectExpressionWithDeepPath = <Result>(
const getRootIdentifierOfDeepMemberExpression = (
deepMemberExpression: DeepMemberExpression | undefined,
): t.Identifier | undefined => {
if (!deepMemberExpressionToPath) return undefined;
if (!deepMemberExpression) return undefined;
if (t.isIdentifier(deepMemberExpression?.node)) {
return deepMemberExpression?.node;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/machine-extractor/src/sky/skyConfigCallExpression.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as t from '@babel/types';
import { createParser } from '../createParser';
import { GetParserResult } from '../utils';
import { LiveNode } from './skyConfigNode';
import { SkyNode } from './skyConfigNode';
import { ALLOWED_SKY_CONFIG_CALL_EXPRESSION_NAMES } from './skyConfigUtils';

export type TMachineCallExpression = GetParserResult<
Expand All @@ -21,7 +21,7 @@ export const SkyConfigCallExpression = createParser({
return {
callee: node.callee,
calleeName: node.callee.property.name,
definition: LiveNode.parse(node.arguments[0], context),
definition: SkyNode.parse(node.arguments[0], context),
isMemberExpression: true,
node,
};
Expand All @@ -34,7 +34,7 @@ export const SkyConfigCallExpression = createParser({
return {
callee: node.callee,
calleeName: node.callee.name,
definition: LiveNode.parse(node.arguments[0], context),
definition: SkyNode.parse(node.arguments[0], context),
isMemberExpression: false,
node,
};
Expand Down
12 changes: 11 additions & 1 deletion packages/machine-extractor/src/sky/skyConfigExtractFromFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { getMachineNodesFromFile } from '../getMachineNodesFromFile';
import { skyConfigExtractResult } from './skyConfigExtractResult';
import { ALLOWED_SKY_CONFIG_CALL_EXPRESSION_NAMES } from './skyConfigUtils';

export const skyConfigExtractFromFile = (fileContent: string) => {
export const skyConfigExtractFromFile = ({
fileContent,
filePath,
cwd,
}: {
fileContent: string;
filePath: string;
cwd: string;
}) => {
if (
!ALLOWED_SKY_CONFIG_CALL_EXPRESSION_NAMES.some((name) =>
fileContent.includes(name),
Expand All @@ -18,6 +26,8 @@ export const skyConfigExtractFromFile = (fileContent: string) => {
skyConfigExtractResult({
file,
fileContent,
filePath,
cwd,
node,
}),
),
Expand Down
15 changes: 15 additions & 0 deletions packages/machine-extractor/src/sky/skyConfigExtractResult.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import * as t from '@babel/types';
import { hashedId } from '../utils';
import { SkyConfigCallExpression } from './skyConfigCallExpression';
import { getClosestEnvFile, getEnvValue } from './skyEnvFileUtils';

export function skyConfigExtractResult({
file,
fileContent,
filePath,
cwd,
node,
}: {
file: t.File;
fileContent: string;
filePath: string;
cwd: string;
node: t.CallExpression;
}) {
const skyConfigCallResult = SkyConfigCallExpression.parse(node, {
Expand All @@ -17,6 +22,16 @@ export function skyConfigExtractResult({
const fileText = fileContent.substring(node.start!, node.end!);
return hashedId(fileText);
},
getNodeSource: (node: t.Node): string => {
return fileContent.substring(node.start!, node.end!);
},
getEnvVariable: (name: string): string | undefined => {
if (process.env[name]) {
return process.env[name];
}
const envFileContents = getClosestEnvFile({ filePath, cwd });
return getEnvValue(envFileContents, name);
},
});
return skyConfigCallResult && skyConfigCallResult.definition;
}
46 changes: 43 additions & 3 deletions packages/machine-extractor/src/sky/skyConfigNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as t from '@babel/types';
import { createParser } from '../createParser';
import { maybeIdentifierTo } from '../identifiers';
import { StringLiteral } from '../scalars';
import { AnyParser } from '../types';
import { maybeTsAsExpression } from '../tsAsExpression';
import { AnyParser, StringLiteralNode } from '../types';
import { unionType } from '../unionType';
import {
GetParserResult,
ObjectPropertyInfo,
Expand All @@ -18,11 +22,47 @@ export type SkyConfigNodeReturn = WithValueNodes<{
}> &
Pick<ObjectPropertyInfo, 'node'>;

const StringLiteralOrEnvKey = unionType([
StringLiteral,
maybeTsAsExpression(
maybeIdentifierTo(
createParser({
babelMatcher: t.isMemberExpression,
parseNode: (node, context): StringLiteralNode => {
if (!context.getNodeSource || !context.getEnvVariable) {
throw new Error("Couldn't find API key in any of the env files");
}

// Let's find the last part of the expression (identifier), e.g. `API_KEY` in `process.env.API_KEY`
const envVariableName =
(t.isMetaProperty(node.object) ||
t.buildMatchMemberExpression('process.env')(node.object)) &&
t.isIdentifier(node.property)
? node.property.name
: null;

if (envVariableName) {
const value = context.getEnvVariable(envVariableName);
if (!value) {
throw new Error("Couldn't find API key in any of the env files");
}
return { value, node };
} else {
throw new Error(
'Invalid API key, we support strings or reading from process.env.YOUR_KEY_HERE',
);
}
},
}),
),
),
]);

const SkyConfigNodeObject: AnyParser<SkyConfigNodeReturn> =
objectTypeWithKnownKeys(() => ({
url: StringLiteral,
apiKey: StringLiteral,
apiKey: StringLiteralOrEnvKey,
xstateVersion: StringLiteral,
}));

export const LiveNode = SkyConfigNodeObject;
export const SkyNode = SkyConfigNodeObject;
55 changes: 55 additions & 0 deletions packages/machine-extractor/src/sky/skyEnvFileUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as fs from 'fs';
import * as path from 'path';

/**
* Searches for the closest '.env' file starting from the directory of the given file path and moving up the directory tree until the root of the project is reached.
* @param filePath - The path of the file to start searching from.
* @param cwd - The root directory of the project.
* @returns The contents of the closest '.env' file.
* @throws An error if no '.env' file is found in the project.
*/
export function getClosestEnvFile({
filePath,
cwd,
}: {
filePath: string;
cwd: string;
}) {
const startDir = path.dirname(filePath);
if (!fs.existsSync(startDir)) {
throw new Error(`No such directory: ${startDir}`);
} else {
let currentDir = startDir;
let searchingInsideProject = true;
while (searchingInsideProject) {
const fileList = fs.readdirSync(currentDir);
const targetFile = fileList.find((file) => file.startsWith('.env'));
if (targetFile) {
const envFilePath = path.resolve(targetFile);
return fs.readFileSync(envFilePath, 'utf8');
} else {
// If we've reached the root of the project, stop searching
if (path.resolve(currentDir) === cwd) {
searchingInsideProject = false;
} else {
currentDir = path.join(currentDir, '..');
}
}
}

throw new Error("Could not find any '.env' file in the project.");
}
}

/**
* Returns the value of a given key in an environment variable string.
* @param envString - The environment variable string to search in.
* @param key - The key to search for.
* @returns The value of the key if found, otherwise undefined.
*/
export function getEnvValue(envString: string, key: string) {
const match = envString.match(`${key}="(.*)"`);
if (match && match[1]) {
return match[1];
}
}
2 changes: 2 additions & 0 deletions packages/machine-extractor/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface StringLiteralNode {
export interface ParserContext {
file: t.File;
getNodeHash: (node: t.Node) => string;
getNodeSource?: (node: t.Node) => string;
getEnvVariable?: (name: string) => string | undefined;
}

export interface Parser<T extends t.Node = any, Result = any> {
Expand Down
1 change: 0 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"main": "dist/xstate-tools-shared.cjs.js",
"license": "MIT",
"dependencies": {
"@statelyai/sky": "0.0.4",
"@xstate/machine-extractor": "^0.12.1"
},
"scripts": {
Expand Down
1 change: 0 additions & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ export * from './isCursorInPosition';
export * from './processFileEdits';
export * from './resolveUriToFilePrefix';
export * from './types';
export * from './writeToFetchedMachineFile';
46 changes: 15 additions & 31 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1773,14 +1773,14 @@
dependencies:
"@sinonjs/commons" "^1.7.0"

"@statelyai/[email protected].4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@statelyai/sky/-/sky-0.0.4.tgz#c23aad7544e117f735ebe2b30f3064e7bc2dcc1e"
integrity sha512-KCmcVOsJlVdsRbgv9G+1LrbeRH8Tp4pqDhq/i9stpnURS9hRbs1gAFfUDmKvWHMnd6785FkRXQv80fyi+82wGw==
"@statelyai/[email protected].7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@statelyai/sky/-/sky-0.0.7.tgz#d5f856eabb49f156e0d58d3d7a39cbf458675a95"
integrity sha512-ZpPZiqrwuoemuVnomP2EhfUvkmDGILlFBkulasYcBQ/AZGMr+K3PIoOBBdokRcq9+Qma+3n0irXMO0xYB5iIww==
dependencies:
partysocket "0.0.9"
partysocket "0.0.12"
superjson "1.13.1"
xstate "5.0.0-beta.33"
xstate "5.0.0-beta.37"

"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
Expand Down Expand Up @@ -5114,7 +5114,7 @@ jest@^27.4.7:
import-local "^3.0.2"
jest-cli "^27.4.7"

"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
Expand Down Expand Up @@ -5392,13 +5392,6 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"

loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"

lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
Expand Down Expand Up @@ -5973,12 +5966,10 @@ [email protected]:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==

[email protected]:
version "0.0.9"
resolved "https://registry.yarnpkg.com/partysocket/-/partysocket-0.0.9.tgz#8c898ae5697bac76d3c3569c4671401c3b575cea"
integrity sha512-SVFtPseMU9DC7fd7MdMx/y9MgWFMteWBgL0wVDQ90nCRBzt/pD1x8ovIqsCFK7uo1KXOXvqJuf9vjTIGPw77Wg==
dependencies:
react "^18.2.0"
[email protected]:
version "0.0.12"
resolved "https://registry.yarnpkg.com/partysocket/-/partysocket-0.0.12.tgz#97285d8047121fe82d8294f46dea037f579c5b22"
integrity sha512-20fikH08aHl7oJvRD7QLftzHvCEqxHf6y/ZiO0wltkl2cbNbtZQuDno9CLrqIQG2t22UWFHjiSqj/rT1In36Eg==

pascalcase@^0.1.1:
version "0.1.1"
Expand Down Expand Up @@ -6210,13 +6201,6 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==

react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"

read-pkg-up@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
Expand Down Expand Up @@ -7693,10 +7677,10 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.30.tgz#8eb7e8551ac6dd24e06bcdfdc834ce6aea3f1f5f"
integrity sha512-ZWNf48jEZxasco7oi4vyKJRgjpwc1+Q0AgzfVF+nrWeghAwc8oW+W2rBQaQWldyZ9zXORBzX7xZAfgu81oANkA==

[email protected].33:
version "5.0.0-beta.33"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.33.tgz#2f25ce90cc0d7b1c84a5183347f5cecb980e065e"
integrity sha512-zHwbY2d2GGrsIySUCybrlq6YAPGM20yKpvliroDqfSbwa255Z1d7RYLkbbxiLx8SnEwDpWVple7JTXkjOw3JLA==
[email protected].37:
version "5.0.0-beta.37"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.0.0-beta.37.tgz#617dc79ec2dc6b30b2567da93c0559bf2d595b34"
integrity sha512-pbdl6piQ7H+KGd8TKxo5htomMkP85HXLVfTuUy6J0JMPvI8srum3YD+QINx3Ea4QAib8tqk28u70BNq2opnoIw==

xstate@^4.33.4:
version "4.33.4"
Expand Down

0 comments on commit 9789cd7

Please sign in to comment.