UNPKG

@stackbit/sdk

Version:
285 lines (257 loc) 10.4 kB
import path from 'path'; import _ from 'lodash'; import { reducePromise } from '@stackbit/utils'; import estree from 'estree'; // not sure why, but using import acorn from 'acorn' doesn't work const acorn = require('acorn'); import { FileBrowser } from './file-browser'; export async function findDirsWithPackageDependency(fileBrowser: FileBrowser, packageNames: string[]): Promise<string[]> { const fileName = 'package.json'; const packageJsonExists = fileBrowser.fileNameExists(fileName); if (!packageJsonExists) { return []; } let filePaths = fileBrowser.getFilePathsForFileName(fileName); filePaths = await reducePromise( filePaths, async (filePaths: string[], filePath: string) => { const data = await fileBrowser.getFileData(filePath); const hasDependency = _.some(packageNames, (packageName) => _.has(data, ['dependencies', packageName])); const hasDevDependency = _.some(packageNames, (packageName) => _.has(data, ['devDependencies', packageName])); if (hasDependency || hasDevDependency) { filePaths.push(filePath); } return filePaths; }, [] ); return _.map(filePaths, (filePath) => path.parse(filePath).dir); } export async function extractNodeEnvironmentVariablesFromFile(data: string): Promise<string[]> { const envVars: string[] = []; const envVarsRe = /process\.env\.(\w+)/g; let reResult; while ((reResult = envVarsRe.exec(data)) !== null) { envVars.push(reResult[1]!); } return _.uniq(envVars); } interface GatsbySourceFilesystemOptions { name: string; path: string; ignore?: string[]; } export function getGatsbySourceFilesystemOptions(data: string): GatsbySourceFilesystemOptions[] { // Use https://astexplorer.net/ with "JavaScript" and "acorn" parser to generate ESTree const ast = acorn.parse(data, { ecmaVersion: 2020 }) as unknown as estree.Program; // find an object having the following format: // { // resolve: 'gatsby-source-filesystem', // options: { // path: `${__dirname}/content`, // name: 'pages' // } // } const result: GatsbySourceFilesystemOptions[] = []; traverseESTree( ast, (node: estree.Node) => { if (!isObjectExpressionNode(node)) { return true; } const resolveProperty = findObjectProperty(node, 'resolve'); if (!resolveProperty) { return true; } // we found an object with 'resolve' property, which is one of the plugins // from now on, return false to not continue traversing the current subtree const isGatsbySourceFileSystem = propertyValueEqual(resolveProperty, 'gatsby-source-filesystem'); if (!isGatsbySourceFileSystem) { return false; } const optionsProperty = findObjectProperty(node, 'options'); if (!optionsProperty || !isObjectExpressionNode(optionsProperty.value)) { return false; } const pathProperty = findObjectProperty(optionsProperty.value, 'path'); const nameProperty = findObjectProperty(optionsProperty.value, 'name'); if (!pathProperty || !nameProperty) { return false; } let pathValue = getNodeValue(pathProperty.value); const nameValue = getNodeValue(nameProperty.value); if (typeof pathValue !== 'string' || typeof nameValue !== 'string') { return false; } pathValue = pathValue.replace(/^\${__dirname}\//, ''); const ignoreProperty = findObjectProperty(optionsProperty.value, 'ignore'); const ignoreValue = ignoreProperty ? getNodeValue(ignoreProperty.value) : null; result.push({ name: nameValue, path: pathValue, ...(isStringArray(ignoreValue) ? { ignore: ignoreValue } : {}) }); }, { iteratePrimitives: false } ); return result; } function findObjectProperty(node: estree.ObjectExpression, propertyName: string) { return _.find(node.properties, (property): property is estree.Property => { return isPropertyNode(property) && propertyNameEqual(property, propertyName); }); } function propertyNameEqual(property: estree.Property, propertyName: string) { // check both identifier and literal properties // { propertyName: '...' } OR { 'propertyName': '...' } return (isIdentifierNode(property.key) && property.key.name === propertyName) || (isLiteralNode(property.key) && property.key.value === propertyName); } function propertyValueEqual(property: estree.Property, propertyValue: string) { // check both literal and template literals values // { propertyName: 'propertyValue' } OR { propertyName: `propertyValue` } if (isLiteralNode(property.value) && property.value.value === propertyValue) { return true; } if (isTemplateLiteralNode(property.value)) { const value = property.value; return ( value.expressions.length === 0 && value.quasis.length === 1 && isTemplateElementNode(value.quasis[0]!) && value.quasis[0]!.value.raw === propertyValue ); } return false; } /** * This method doesn't serialize every possible ESTree node value. It only * serializes literals, template literals and array expressions needed to * extract simple hard-coded values. * * If this method cannot serialize a value, it returns undefined */ function getNodeValue(node: estree.Node): any { if (isLiteralNode(node)) { return node.value; } else if (isTemplateLiteralNode(node)) { const expressions = node.expressions; const quasis = node.quasis; const sortedNodes = _.sortBy([...expressions, ...quasis], 'start'); return _.reduce( sortedNodes, (result: string | undefined, node) => { if (result === undefined) { return result; } if (isTemplateElementNode(node)) { return result + node.value.raw; } else if (isIdentifierNode(node)) { return result + '${' + node.name + '}'; } return undefined; }, '' ); } else if (isArrayExpressionNode(node)) { return _.reduce( node.elements, (result: any[] | undefined, node) => { if (result === undefined) { return result; } if (node === null) { return undefined; } const value = getNodeValue(node); if (value === undefined) { return value; } result.push(value); return result; }, [] ); } return undefined; } function isObjectExpressionNode(node: estree.Node): node is estree.ObjectExpression { return node.type === 'ObjectExpression'; } function isArrayExpressionNode(node: estree.Node): node is estree.ArrayExpression { return node.type === 'ArrayExpression'; } function isIdentifierNode(node: estree.Node): node is estree.Identifier { return node.type === 'Identifier'; } function isLiteralNode(node: estree.Node): node is estree.Literal { return node.type === 'Literal'; } function isPropertyNode(node: estree.Node): node is estree.Property { return node.type === 'Property'; } function isTemplateLiteralNode(node: estree.Node): node is estree.TemplateLiteral { return node.type === 'TemplateLiteral'; } function isTemplateElementNode(node: estree.Node): node is estree.TemplateElement { return node.type === 'TemplateElement'; } function isStringArray(value: any): value is string[] { return _.isArray(value) && _.every(value, _.isString); } function traverseESTree( value: any, iteratee: (value: any, keyPath: (string | number)[], stack: any[]) => any, options: { iterateCollections?: boolean; iteratePrimitives?: boolean } = {} ) { const context = _.get(options, 'context'); const iterateCollections = _.get(options, 'iterateCollections', true); const iteratePrimitives = _.get(options, 'iteratePrimitives', true); function _traverse(value: any, keyPath: (string | number)[], stack: any[]) { const isArrayOrObject = _.isPlainObject(value) || _.isArray(value) || value instanceof acorn.Node; const invokeIteratee = isArrayOrObject ? iterateCollections : iteratePrimitives; let continueTraversing = true; if (invokeIteratee) { continueTraversing = iteratee.call(context, value, keyPath, stack); } if (isArrayOrObject && continueTraversing) { _.forEach(value, (val: any, key: string | number) => { _traverse(val, _.concat(keyPath, key), _.concat(stack, [value])); }); } } _traverse(value, [], []); } export function getGatsbySourceFilesystemOptionsUsingRegExp(data: string) { // { // resolve: `gatsby-source-filesystem`, // options: { // name: 'pages', // path: `${__dirname}/src/pages` // } // } // eslint-disable-next-line const gatsbySourceFilesystemRegExp = /resolve\s*:\s*(['"`])gatsby-source-filesystem\1\s*,\s*options\s*:\s*{\s*(\w+)\s*:\s*(['"`])([^'"`]+)\3\s*,\s*(\w+)\s*:\s*(['"`])([^'"`]+)\6/g; let match: RegExpExecArray | null; const fileSystemOptions = []; while ((match = gatsbySourceFilesystemRegExp.exec(data)) !== null) { const option1 = match[2]; const option2 = match[5]; const value1 = match[4]; const value2 = match[7]; if (option1 === 'name' && option2 === 'path' && value1 && value2) { fileSystemOptions.push({ name: value1, path: value2 }); } else if (option1 === 'path' && option2 === 'name' && value1 && value2) { fileSystemOptions.push({ name: value2, path: value1 }); } } return fileSystemOptions; }