UNPKG

scraipt

Version:

Scrape away inefficient code during compile-time using AI

184 lines (183 loc) 8.41 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); // Setup dotenv before anything else to ensure environment variables are available const dotenv_1 = require("dotenv"); (0, dotenv_1.config)(); const core_1 = require("@babel/core"); const generator_1 = __importDefault(require("@babel/generator")); const OpenAI_1 = require("./modules/OpenAI/OpenAI"); const codeProcessing_1 = require("./modules/codeProcessing"); const fileUtils_1 = require("./modules/fileUtils"); const cache_1 = require("./modules/cache"); const chalk_1 = __importDefault(require("chalk")); const openai = new OpenAI_1.OpenAIAPI(); const cache = new cache_1.Cache(); /** If the line above the node contains a comment with the text "scraipt-pass", the node should be left as is. * @param node The node to check. * @param source The source code. * @returns True if the node should be left as is, false otherwise. */ const shouldPassNode = (node, source) => { var _a; if ((_a = !node) !== null && _a !== void 0 ? _a : !node.loc) { return false; } const previousLine = (0, codeProcessing_1.getSourceAtLine)(source, node.loc.start.line - 1); if (!previousLine) { return false; } return /(\/\*)|(\/\/)*(.*?scraipt-pass)/.test(previousLine); }; /** Create a source context by removing the node source from the source code. * @param source The source code. * @param nodeSource The source code of the node. * @returns The source context. */ const createSourceContext = (source, nodeSource) => { // TODO: This is a bit janky, we should use the AST to remove the node instead of using string manipulation return source.replace(nodeSource, ''); }; /** Transform the given source code to be more efficient and optimized using all available resources. * @param source The source code to transform. * @param path The path of the source code. If not provided, the source code will not be written to a file. * @returns The transformed source code. */ const transformSourceCode = (source, options, resourcePath) => __awaiter(void 0, void 0, void 0, function* () { var _a; if (!source || !options || !resourcePath) { return source; } // Setup OpenAI parameters const model = (_a = options.model) !== null && _a !== void 0 ? _a : 'gpt-4'; openai.maxTokenCount = options.maxTokens; // Only include files listed in the include option if (options.include) { for (const include of options.include) { if (!resourcePath.includes(include)) { return source; } // TODO: minimatch for glob patterns // if (!minimatch(resourcePath, include)) { // return source; // } } } const AST = (0, codeProcessing_1.parseSource)(source); if (!AST) { return source; } const workers = []; let transformedSource = source; (0, core_1.traverse)(AST, { // No need for asynchroneous execution as we can just run the code synchronously and wait for the result since we aren't modifying the AST enter(path) { const worker = () => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; const isArrowFunction = path.isArrowFunctionExpression(); // If the node is not a function, skip it if (!path.isFunctionDeclaration() && !isArrowFunction) return; const node = path.node; if ((_c = (_b = (_a = !node) !== null && _a !== void 0 ? _a : !node.start) !== null && _b !== void 0 ? _b : !node.end) !== null && _c !== void 0 ? _c : !node.loc) { return; } // Get the actual source code of the node const nodeSource = source.substring(node.start, node.end); if (shouldPassNode(node, source)) { return; } // Check for the node in the cache const cachedNode = cache.get(nodeSource); if (cachedNode) { transformedSource = source.replace(nodeSource, cachedNode); return; } // Source with the node removed const sourceContext = createSourceContext ? createSourceContext(source, nodeSource) : source; if (options.dryRun) { return; } // Generate the optimized version of the code const completion = yield openai.createTextCompletion(sourceContext, nodeSource, model); if (!completion) { return; } const completionAST = (0, codeProcessing_1.parseSource)(completion); if (!completionAST) { return; } const completionFunctionAST = completionAST .program.body[0]; if (!completionFunctionAST) { return; } const completionBodyAST = completionFunctionAST.body; if (!completionBodyAST) { return; } const completionBody = (0, generator_1.default)(completionBodyAST).code; if (!completionBody) { return; } const originalNodeBodyAST = node.body; const originalNodeBody = (0, generator_1.default)(originalNodeBodyAST).code; // Get the source code of the function decleration const nodeFunctionDeclerationLine = (0, codeProcessing_1.getSourceAtLine)(source, node.loc.start.line); // A litle janky but it works const originalParams = nodeFunctionDeclerationLine .split('(')[1] .split(')')[0]; let nodeName; if (!isArrowFunction) { nodeName = (_d = node.id) === null || _d === void 0 ? void 0 : _d.name; } const transformedNodeSource = (0, codeProcessing_1.createFunction)(nodeName, originalParams, completionBody, originalNodeBody, isArrowFunction); // Replace the original source code with the optimized version transformedSource = source.replace(nodeSource, transformedNodeSource); // Add to the cache cache.set(nodeSource, transformedNodeSource); }); workers.push(worker); }, }); yield Promise.all(workers.map((worker) => worker())); // Check if the transformed code is valid javascript if (!(0, codeProcessing_1.isValidJavaScript)(transformedSource)) { return source; } if (options.debug) { (0, fileUtils_1.writeScraiptFile)(resourcePath, transformedSource, options.buildPath); } if (options.dryRun) { console.log(`${chalk_1.default.blue('Scraipt')} Would have wrote: ${resourcePath}`); return source; } return transformedSource; }); // Main webpack loader function module.exports = function (source) { // Not sure why but "this" needs to be casted to "any" to avoid type errors const options = this.getOptions(); const resourcePath = this.resourcePath; if (options.debug) { (0, fileUtils_1.createScraiptFolder)(options.buildPath); } return transformSourceCode(source, options, resourcePath); };