From 45627bef102fd0e6efd30f24c7296bf785029e3a Mon Sep 17 00:00:00 2001 From: Nikhil Saraf Date: Thu, 28 Dec 2023 01:52:24 +0530 Subject: [PATCH] (fix): decorate exports use parse-advanced, fixes #36 --- .../fixtures/decorate-exports.snapshot.ts | 113 ++++ .../fixtures/decorate-exports.ts | 107 +++ packages/vinxi-directives/index.js | 9 +- .../plugins/decorate-exports.js | 161 +++++ .../{ => plugins}/shim-exports.js | 4 +- .../{ => plugins}/wrap-exports.js | 4 +- packages/vinxi-directives/transform.js | 618 ------------------ packages/vinxi-directives/transform.test.js | 15 +- packages/vinxi-directives/utils.js | 137 ++++ 9 files changed, 538 insertions(+), 630 deletions(-) create mode 100644 packages/vinxi-directives/fixtures/decorate-exports.snapshot.ts create mode 100644 packages/vinxi-directives/fixtures/decorate-exports.ts create mode 100644 packages/vinxi-directives/plugins/decorate-exports.js rename packages/vinxi-directives/{ => plugins}/shim-exports.js (98%) rename packages/vinxi-directives/{ => plugins}/wrap-exports.js (99%) delete mode 100644 packages/vinxi-directives/transform.js create mode 100644 packages/vinxi-directives/utils.js diff --git a/packages/vinxi-directives/fixtures/decorate-exports.snapshot.ts b/packages/vinxi-directives/fixtures/decorate-exports.snapshot.ts new file mode 100644 index 00000000..845c069f --- /dev/null +++ b/packages/vinxi-directives/fixtures/decorate-exports.snapshot.ts @@ -0,0 +1,113 @@ +import { createReference } from '~/runtime'; + +import { cache } from "@solidjs/router"; +import { consola } from "consola"; +import { createStorage } from "unstorage"; +import redisDriver from "unstorage/drivers/redis"; +import type { MenuItem, Settings } from "~/types/common"; + +const debugFetcher = import.meta.env.VITE_COG_DEBUG_FETCHER == 1; +const cacheEnabled = import.meta.env.VITE_COG_CACHE_ENABLED == 1; +const cacheDebug = import.meta.env.VITE_COG_CACHE_DEBUG == 1; + +const storage = createStorage({ + driver: redisDriver({ + base: import.meta.env.VITE_KB_REDIS_BASE ?? "cog:", + host: import.meta.env.VITE_KB_REDIS_HOST ?? "localhost", + port: import.meta.env.VITE_KB_REDIS_PORT ?? 6379, + }), +}); + +async function fetchAPI(path: string) { + const headers = new Headers(); + headers.append("User-Agent", "chrome"); + + if (import.meta.env.VITE_COG_BACKEND_AUTH_LOGIN.length > 0) { + headers.append( + "Authorization", + "Basic " + + btoa( + import.meta.env.VITE_KB_BACKEND_AUTH_LOGIN + + ":" + + import.meta.env.VITE_KB_BACKEND_AUTH_PASSWORD, + ), + ); + } + + let url = `${import.meta.env.VITE_COG_BACKEND_BASE_URL}/${path}/`; + + if (url.indexOf("?") > -1) { + // remove trailing slash + url = url.replace(/\/$/, ""); + } + + try { + debugFetcher && consola.info(`Fetching ${url}`); + const response = await fetch(url, { headers }); + + // @TODO we should probably cache error responses for a short period of time + if ("error" in response) { + return response; + } + + if (response.status !== 200) { + // @TODO we should probably cache error responses for a short period of time + return { + code: response.status, + error: `Server responded with status ${response.status}`, + }; + } + + const json = await response.json().catch((error: string): void => { + throw new Error(`Was Fetching ${url}, got ${error}`); + }); + + if (!("data" in json)) { + return { + code: 500, + error: `JSON response does not contain data`, + }; + } + + if (cacheEnabled) { + await storage.setItem(path, json.data); + } + + return json.data; + } catch (error) { + return { error }; + } +} + +export const getDataAtPath = async (path: string) => { + "use runtime"; + + if (cacheEnabled) { + const data = await storage.getItem(path); + if (data) { + cacheDebug && consola.info(`Cache hit: ${path.slice(0, 80)}…`); + return data; + } + } + + cacheDebug && consola.info(`Cache miss: ${path.slice(0, 80)}…`); + return fetchAPI(path); +}; + +export const getSettings = cache(async (): Promise => { + return getDataAtPath("solid/settings"); +}, "settings"); + +export const getMenuMain = cache(async (): Promise => { + return getDataAtPath("solid/menu/main"); +}, "menu:main"); + +export const getMenuFooter = cache(async (): Promise => { + return getDataAtPath("solid/menu/footer"); +}, "menu:footer"); + + +;if (typeof getDataAtPath === "function") createReference(getDataAtPath,"test", "getDataAtPath"); +if (typeof getSettings === "function") createReference(getSettings,"test", "getSettings"); +if (typeof getMenuMain === "function") createReference(getMenuMain,"test", "getMenuMain"); +if (typeof getMenuFooter === "function") createReference(getMenuFooter,"test", "getMenuFooter"); \ No newline at end of file diff --git a/packages/vinxi-directives/fixtures/decorate-exports.ts b/packages/vinxi-directives/fixtures/decorate-exports.ts new file mode 100644 index 00000000..9301da47 --- /dev/null +++ b/packages/vinxi-directives/fixtures/decorate-exports.ts @@ -0,0 +1,107 @@ +"use runtime"; + +import { cache } from "@solidjs/router"; +import { consola } from "consola"; +import { createStorage } from "unstorage"; +import redisDriver from "unstorage/drivers/redis"; +import type { MenuItem, Settings } from "~/types/common"; + +const debugFetcher = import.meta.env.VITE_COG_DEBUG_FETCHER == 1; +const cacheEnabled = import.meta.env.VITE_COG_CACHE_ENABLED == 1; +const cacheDebug = import.meta.env.VITE_COG_CACHE_DEBUG == 1; + +const storage = createStorage({ + driver: redisDriver({ + base: import.meta.env.VITE_KB_REDIS_BASE ?? "cog:", + host: import.meta.env.VITE_KB_REDIS_HOST ?? "localhost", + port: import.meta.env.VITE_KB_REDIS_PORT ?? 6379, + }), +}); + +async function fetchAPI(path: string) { + const headers = new Headers(); + headers.append("User-Agent", "chrome"); + + if (import.meta.env.VITE_COG_BACKEND_AUTH_LOGIN.length > 0) { + headers.append( + "Authorization", + "Basic " + + btoa( + import.meta.env.VITE_KB_BACKEND_AUTH_LOGIN + + ":" + + import.meta.env.VITE_KB_BACKEND_AUTH_PASSWORD, + ), + ); + } + + let url = `${import.meta.env.VITE_COG_BACKEND_BASE_URL}/${path}/`; + + if (url.indexOf("?") > -1) { + // remove trailing slash + url = url.replace(/\/$/, ""); + } + + try { + debugFetcher && consola.info(`Fetching ${url}`); + const response = await fetch(url, { headers }); + + // @TODO we should probably cache error responses for a short period of time + if ("error" in response) { + return response; + } + + if (response.status !== 200) { + // @TODO we should probably cache error responses for a short period of time + return { + code: response.status, + error: `Server responded with status ${response.status}`, + }; + } + + const json = await response.json().catch((error: string): void => { + throw new Error(`Was Fetching ${url}, got ${error}`); + }); + + if (!("data" in json)) { + return { + code: 500, + error: `JSON response does not contain data`, + }; + } + + if (cacheEnabled) { + await storage.setItem(path, json.data); + } + + return json.data; + } catch (error) { + return { error }; + } +} + +export const getDataAtPath = async (path: string) => { + "use runtime"; + + if (cacheEnabled) { + const data = await storage.getItem(path); + if (data) { + cacheDebug && consola.info(`Cache hit: ${path.slice(0, 80)}…`); + return data; + } + } + + cacheDebug && consola.info(`Cache miss: ${path.slice(0, 80)}…`); + return fetchAPI(path); +}; + +export const getSettings = cache(async (): Promise => { + return getDataAtPath("solid/settings"); +}, "settings"); + +export const getMenuMain = cache(async (): Promise => { + return getDataAtPath("solid/menu/main"); +}, "menu:main"); + +export const getMenuFooter = cache(async (): Promise => { + return getDataAtPath("solid/menu/footer"); +}, "menu:footer"); diff --git a/packages/vinxi-directives/index.js b/packages/vinxi-directives/index.js index 310e83e7..b59e7d39 100644 --- a/packages/vinxi-directives/index.js +++ b/packages/vinxi-directives/index.js @@ -1,4 +1,7 @@ export { directives } from "./plugin.js"; -export { decorateExportsPlugin } from "./transform.js"; -export { wrapExports, wrapExportsPlugin } from "./wrap-exports.js"; -export { shimExports, shimExportsPlugin } from "./shim-exports.js"; +export { + decorateExportsPlugin, + decorateExports, +} from "./plugins/decorate-exports.js"; +export { wrapExports, wrapExportsPlugin } from "./plugins/wrap-exports.js"; +export { shimExports, shimExportsPlugin } from "./plugins/shim-exports.js"; diff --git a/packages/vinxi-directives/plugins/decorate-exports.js b/packages/vinxi-directives/plugins/decorate-exports.js new file mode 100644 index 00000000..2680530c --- /dev/null +++ b/packages/vinxi-directives/plugins/decorate-exports.js @@ -0,0 +1,161 @@ +import { print, types } from "recast"; + +import { parseAdvanced } from "../parse.js"; +import { addLocalExportedNames } from "../utils.js"; + +export function decorateExports({ + code, + id, + ast, + runtime, + hash, + options, + directive, +}) { + // onServerReference(moduleId); + // If the same local name is exported more than once, we only need one of the names. + const localNames = new Map(); + const localTypes = new Map(); + + for (let i = 0; i < ast.program.body.length; i++) { + const node = ast.program.body[i]; + switch (node.type) { + case "ExportAllDeclaration": + // If export * is used, the other file needs to explicitly opt into "use server" too. + break; + case "ExportDefaultDeclaration": + if (node.declaration.type === "Identifier") { + localNames.set(node.declaration.name, "default"); + } else if (node.declaration.type === "FunctionDeclaration") { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, "default"); + localTypes.set(node.declaration.id.name, "function"); + } else { + // TODO: This needs to be rewritten inline because it doesn't have a local name. + } + } + continue; + case "ExportNamedDeclaration": + if (node.declaration) { + if (node.declaration.type === "VariableDeclaration") { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addLocalExportedNames(localNames, declarations[j].id); + } + } else { + const name = node.declaration.id.name; + localNames.set(name, name); + if (node.declaration.type === "FunctionDeclaration") { + localTypes.set(name, "function"); + } + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + localNames.set(specifier.local.name, specifier.exported.name); + } + } + continue; + } + } + + ast.program.body = [ + types.builders.importDeclaration( + [ + types.builders.importSpecifier( + types.builders.identifier(runtime.function), + ), + ], + types.builders.stringLiteral(runtime.module), + ), + ...ast.program.body.filter((node) => node.directive !== directive), + ...[...localNames.entries()].map(([local, exported]) => { + // return an if block that checks if the export is a function and if so annotates it. + return types.builders.ifStatement( + types.builders.binaryExpression( + "===", + types.builders.unaryExpression( + "typeof", + types.builders.identifier(local), + ), + types.builders.stringLiteral("function"), + ), + types.builders.expressionStatement( + types.builders.callExpression( + types.builders.identifier(runtime.function), + [ + types.builders.identifier(local), + types.builders.stringLiteral( + options.command === "build" ? hash(id) : id, + ), + types.builders.stringLiteral(exported), + ], + ), + ), + ); + }), + ]; + + ast.program.directives = ast.program.directives?.filter( + (node) => node.value !== directive, + ); + + let newSrc = print(ast).code; + return newSrc; +} + +export function decorateExportsPlugin({ + runtime, + hash, + pragma, + apply, + onModuleFound, +}) { + return { + name: "decorate-exports", + async transform(code, id, options, ctx) { + if (code.indexOf(pragma) === -1) { + return code; + } + + const shouldApply = apply(code, id, options); + + if (!shouldApply) { + return code; + } + + function hasDir(node) { + return node?.directives?.[0]?.value?.value === pragma; + } + + function hasFunctionDirective(node) { + return hasDir(node.body); + } + + const ast = parseAdvanced(code); + + if (ast.length === 0) { + return code; + } + + if (hasDir(ast.program)) { + ast.program.directives = []; + let result = await decorateExports({ + runtime, + ast, + id, + code, + hash, + options, + directive: pragma, + }); + onModuleFound?.(id); + return result; + } + + return code; + }, + }; +} diff --git a/packages/vinxi-directives/shim-exports.js b/packages/vinxi-directives/plugins/shim-exports.js similarity index 98% rename from packages/vinxi-directives/shim-exports.js rename to packages/vinxi-directives/plugins/shim-exports.js index 4b98dd65..bfc9af94 100644 --- a/packages/vinxi-directives/shim-exports.js +++ b/packages/vinxi-directives/plugins/shim-exports.js @@ -1,7 +1,7 @@ import { print, types, visit } from "recast"; -import { parseAdvanced, parseLoose } from "./parse.js"; -import { parseExportNamesInto } from "./transform.js"; +import { parseAdvanced, parseLoose } from "../parse.js"; +import { parseExportNamesInto } from "../utils.js"; export function shimExportsPlugin({ runtime, diff --git a/packages/vinxi-directives/wrap-exports.js b/packages/vinxi-directives/plugins/wrap-exports.js similarity index 99% rename from packages/vinxi-directives/wrap-exports.js rename to packages/vinxi-directives/plugins/wrap-exports.js index 37c89ada..925f6131 100644 --- a/packages/vinxi-directives/wrap-exports.js +++ b/packages/vinxi-directives/plugins/wrap-exports.js @@ -1,7 +1,7 @@ import { print, types, visit } from "recast"; -import { parseAdvanced } from "./parse.js"; -import { addLocalExportedNames } from "./transform.js"; +import { parseAdvanced } from "../parse.js"; +import { addLocalExportedNames } from "../utils.js"; export function wrapExportsPlugin({ runtime, diff --git a/packages/vinxi-directives/transform.js b/packages/vinxi-directives/transform.js deleted file mode 100644 index da368d6b..00000000 --- a/packages/vinxi-directives/transform.js +++ /dev/null @@ -1,618 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ -import { readFileSync } from "fs"; - -import { parseLoose } from "./parse.js"; - -/** - * - * @param {{ resolve }} vite - * @param {string} specifier - * @param {string} parentURL - * @returns - */ -export async function resolveClientImport(vite, specifier, parentURL) { - const resolved = await vite.resolve(specifier, parentURL, { - skipSelf: true, - }); - - if (!resolved) { - throw new Error("Could not resolve " + specifier + " from " + parentURL); - } - - return { url: resolved.id }; -} - -export async function parseExportNamesInto(vite, ast, names, parentURL) { - for (let i = 0; i < ast.program.body.length; i++) { - const node = ast.program.body[i]; - switch (node.type) { - case "ExportAllDeclaration": - if (node.exported) { - addExportNames(names, node.exported); - continue; - } else { - const { url } = await resolveClientImport( - vite, - node.source.value, - parentURL, - ); - - const clientImportCode = readFileSync(url, "utf8"); - - const childBody = parseLoose(clientImportCode ?? "").body; - - await parseExportNamesInto(vite, childBody, names, url); - continue; - } - case "ExportDefaultDeclaration": - names.push("default"); - continue; - case "ExportNamedDeclaration": - if (node.declaration) { - if (node.declaration.type === "VariableDeclaration") { - const declarations = node.declaration.declarations; - for (let j = 0; j < declarations.length; j++) { - addExportNames(names, declarations[j].id); - } - } else { - addExportNames(names, node.declaration.id); - } - } - if (node.specifiers) { - const specifiers = node.specifiers; - for (let j = 0; j < specifiers.length; j++) { - addExportNames(names, specifiers[j].exported); - } - } - continue; - } - } -} - -export function addLocalExportedNames(names, node) { - switch (node.type) { - case "Identifier": - names.set(node.name, node.name); - return; - case "ObjectPattern": - for (let i = 0; i < node.properties.length; i++) - addLocalExportedNames(names, node.properties[i]); - return; - case "ArrayPattern": - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i]; - if (element) addLocalExportedNames(names, element); - } - return; - case "Property": - addLocalExportedNames(names, node.value); - return; - case "AssignmentPattern": - addLocalExportedNames(names, node.left); - return; - case "RestElement": - addLocalExportedNames(names, node.argument); - return; - case "ParenthesizedExpression": - addLocalExportedNames(names, node.expression); - return; - } -} - -export function addExportNames(names, node) { - switch (node.type) { - case "Identifier": - names.push(node.name); - return; - case "ObjectPattern": - for (let i = 0; i < node.properties.length; i++) - addExportNames(names, node.properties[i]); - return; - case "ArrayPattern": - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i]; - if (element) addExportNames(names, element); - } - return; - case "Property": - addExportNames(names, node.value); - return; - case "AssignmentPattern": - addExportNames(names, node.left); - return; - case "RestElement": - addExportNames(names, node.argument); - return; - case "ParenthesizedExpression": - addExportNames(names, node.expression); - return; - } -} - -export function decorateExports({ code, id, ast, runtime, hash, options }) { - // onServerReference(moduleId); - - // If the same local name is exported more than once, we only need one of the names. - const localNames = new Map(); - const localTypes = new Map(); - - for (let i = 0; i < ast.length; i++) { - const node = ast[i]; - switch (node.type) { - case "ExportAllDeclaration": - // If export * is used, the other file needs to explicitly opt into "use server" too. - break; - case "ExportDefaultDeclaration": - if (node.declaration.type === "Identifier") { - localNames.set(node.declaration.name, "default"); - } else if (node.declaration.type === "FunctionDeclaration") { - if (node.declaration.id) { - localNames.set(node.declaration.id.name, "default"); - localTypes.set(node.declaration.id.name, "function"); - } else { - // TODO: This needs to be rewritten inline because it doesn't have a local name. - } - } - continue; - case "ExportNamedDeclaration": - if (node.declaration) { - if (node.declaration.type === "VariableDeclaration") { - const declarations = node.declaration.declarations; - for (let j = 0; j < declarations.length; j++) { - addLocalExportedNames(localNames, declarations[j].id); - } - } else { - const name = node.declaration.id.name; - localNames.set(name, name); - if (node.declaration.type === "FunctionDeclaration") { - localTypes.set(name, "function"); - } - } - } - if (node.specifiers) { - const specifiers = node.specifiers; - for (let j = 0; j < specifiers.length; j++) { - const specifier = specifiers[j]; - localNames.set(specifier.local.name, specifier.exported.name); - } - } - continue; - } - } - - let newSrc = - `import { ${runtime.function} } from '${runtime.module}';\n` + - code + - "\n\n;"; - localNames.forEach(function (exported, local) { - if (localTypes.get(local) !== "function") { - // We first check if the export is a function and if so annotate it. - newSrc += "if (typeof " + local + ' === "function") '; - } - newSrc += `${runtime.function}(` + local + ","; - newSrc += `"${ - options.command === "build" ? hash(id) : id - }", "${exported}");\n`; - }); - return newSrc; -} - -// export function splitPlugin({ runtime, hash, pragma, apply, onModuleFound }) { -// return { -// name: "split-exports", -// async split(code, id, options) { -// if (code.indexOf(pragma) === -1) { -// return code; -// } - -// const shouldApply = apply(code, id, options); - -// if (!shouldApply) { -// return code; -// } - -// const ast = parseLoose(code, { -// ecmaVersion: "2024", -// sourceType: "module", -// }); - -// if (ast.length === 0) { -// return code; -// } - -// const body = ast.body; - -// let needsReference = false; -// let splits = 0; - -// let pickedFn = null; - -// visit(body, { -// visitExportNamedDeclaration(path) { -// if ( -// path.node.declaration && -// path.node.declaration.type === "FunctionDeclaration" -// ) { -// const name = path.node.declaration.id?.name.toString(); -// if (hasDirective(path.node.declaration)) { -// needsReference = true; -// if (splits === options.split) { -// pickedFn = types.builders.exportDefaultDeclaration( -// types.builders.functionDeclaration.from({ -// async: true, -// id: name ? types.builders.identifier(name) : null, -// params: path.node.declaration.params, -// body: types.builders.blockStatement( -// path.node.declaration.body.body.slice(1), -// ), -// }), -// ); -// } -// splits++; -// return false; -// } -// } - -// return this.traverse(path); -// }, -// visitFunctionDeclaration(path) { -// const statements = path.get("body", "body", 0); -// const name = path.node.id; -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// if (splits === options.split) { -// pickedFn = types.builders.exportDefaultDeclaration( -// types.builders.functionDeclaration.from({ -// async: true, -// id: name, -// params: path.node.params, -// body: types.builders.blockStatement( -// path.node.body.body.slice(1), -// ), -// }), -// ); -// } -// splits++; -// return false; -// } -// return this.traverse(path); -// }, -// visitArrowFunctionExpression(path) { -// const statements = path.get("body", "body", 0); -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// if (splits === options.split) { -// pickedFn = types.builders.exportDefaultDeclaration( -// types.builders.arrowFunctionExpression( -// path.node.params, -// types.builders.blockStatement(path.node.body.body.slice(1)), -// ), -// ); -// } -// splits++; -// return false; -// } -// return false; -// }, -// // visitFunctionExpression(path) { -// // const name = path.node.id?.name.toString(); -// // const statements = path.get("body", "body", 0); -// // if ( -// // statements.node.type === "ExpressionStatement" && -// // statements.node.directive == pragma -// // ) { -// // needsReference = true; -// // path.replace( -// // types.builders.callExpression( -// // types.builders.identifier(runtime.function), -// // [ -// // types.builders.arrowFunctionExpression( -// // [], -// // types.builders.blockStatement([]), -// // ), -// // types.builders.stringLiteral( -// // options.command === "build" -// // ? hash(id) -// // : id + `?split=${splits++}`, -// // ), -// // types.builders.stringLiteral("default"), -// // ], -// // ), -// // ); -// // this.traverse(path); -// // } -// // return false; -// // }, -// }); - -// ast.body = [pickedFn]; - -// // if (needsReference) { -// // return ( -// // `import { ${runtime.function} } from '${runtime.module}';\n` + -// // print(ast).code -// // ); -// // } - -// return print(ast).code; -// }, -// }; -// } - -// export function shimExportsPlugin({ -// runtime, -// hash, -// pragma, -// apply, -// onModuleFound, -// }) { -// return { -// name: "shim-exports", -// async transform(code, id, options, applied) { -// if (code.indexOf(pragma) === -1) { -// return false; -// } - -// const shouldApply = apply(code, id, options); - -// if (!shouldApply) { -// return false; -// } - -// const ast = parseLoose(code, { -// ecmaVersion: "2024", -// sourceType: "module", -// }); - -// if (ast.length === 0) { -// return; -// } - -// const body = ast.body; - -// for (let i = 0; i < body.length; i++) { -// const node = body[i]; -// if (node.type !== "ExpressionStatement" || !node.directive) { -// break; -// } -// if (node.directive === pragma) { -// onModuleFound?.(id); -// return await shimExports({ -// runtime, -// ast: body, -// id, -// code, -// hash, -// options, -// }); -// } -// } - -// let needsReference = false; -// let splits = 0; - -// visit(body, { -// visitExportNamedDeclaration(path) { -// if ( -// path.node.declaration && -// path.node.declaration.type === "FunctionDeclaration" -// ) { -// const name = path.node.declaration.id?.name.toString(); -// const statements = path.get("declaration", "body", "body", 0); -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// path.replace( -// types.builders.exportNamedDeclaration( -// types.builders.variableDeclaration("const", [ -// types.builders.variableDeclarator( -// types.builders.identifier(name), -// types.builders.callExpression( -// types.builders.identifier(runtime.function), -// [ -// types.builders.functionExpression( -// name ? types.builders.identifier(name) : null, -// [], -// types.builders.blockStatement( -// path.node.declaration.body.body.slice(1), -// ), -// ), -// types.builders.stringLiteral( -// options.command === "build" -// ? hash(id) -// : id + `?split=${splits++}`, -// ), -// types.builders.stringLiteral("default"), -// ], -// ), -// ), -// ]), -// ), -// ); -// } -// } - -// return this.traverse(path); -// }, -// visitFunctionDeclaration(path) { -// const statements = path.get("body", "body", 0); -// const name = path.node.id; -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// path.replace( -// types.builders.variableDeclaration("const", [ -// types.builders.variableDeclarator( -// name, -// types.builders.callExpression( -// types.builders.identifier(runtime.function), -// [ -// types.builders.functionExpression( -// name, -// [], -// types.builders.blockStatement( -// path.node.body.body.slice(1), -// ), -// ), -// types.builders.stringLiteral( -// options.command === "build" -// ? hash(id) -// : id + `?split=${splits++}`, -// ), -// types.builders.stringLiteral("default"), -// ], -// ), -// ), -// ]), -// ); -// this.traverse(path); -// } -// }, -// // } -// // return false; -// // }, -// visitArrowFunctionExpression(path) { -// const statements = path.get("body", "body", 0); -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// path.replace( -// types.builders.callExpression( -// types.builders.identifier(runtime.function), -// [ -// types.builders.arrowFunctionExpression( -// [], -// types.builders.blockStatement(path.node.body.body.slice(1)), -// ), -// types.builders.stringLiteral( -// options.command === "build" -// ? hash(id) -// : id + `?split=${splits++}`, -// ), -// types.builders.stringLiteral("default"), -// ], -// ), -// ); -// this.traverse(path); -// } -// return false; -// }, -// visitFunctionExpression(path) { -// const name = path.node.id?.name.toString(); -// const statements = path.get("body", "body", 0); -// if ( -// statements.node.type === "ExpressionStatement" && -// statements.node.directive == pragma -// ) { -// needsReference = true; -// path.replace( -// types.builders.callExpression( -// types.builders.identifier(runtime.function), -// [ -// types.builders.functionExpression( -// name ? types.builders.identifier(name) : null, -// [], -// types.builders.blockStatement(path.node.body.body.slice(1)), -// ), -// types.builders.stringLiteral( -// options.command === "build" -// ? hash(id) -// : id + `?split=${splits++}`, -// ), -// types.builders.stringLiteral("default"), -// ], -// ), -// ); -// this.traverse(path); -// } -// return false; -// }, -// }); - -// ast.body = body; - -// if (needsReference) { -// return ( -// `import { ${runtime.function} } from '${runtime.module}';\n` + -// print(ast).code -// ); -// } - -// return code; - -// // const body = await shimExports({ -// // runtime, -// // ast, -// // id, -// // code, -// // hash, -// // options, -// // }); -// // return body; -// }, -// }; -// } - -export function decorateExportsPlugin({ - runtime, - hash, - pragma, - apply, - onModuleFound, -}) { - return { - name: "decorate-exports", - async transform(code, id, options, ctx) { - if (code.indexOf(pragma) === -1) { - return code; - } - - const shouldApply = apply(code, id, options); - - if (!shouldApply) { - return code; - } - - const body = parseLoose(code).body; - - for (let i = 0; i < body.length; i++) { - const node = body[i]; - if (node.type !== "ExpressionStatement" || !node.directive) { - break; - } - if (node.directive === pragma) { - onModuleFound?.(id); - return await decorateExports({ - runtime, - ast: body, - id, - code, - hash, - options, - }); - } - } - - return code; - }, - }; -} diff --git a/packages/vinxi-directives/transform.test.js b/packages/vinxi-directives/transform.test.js index d1a844ed..41046491 100644 --- a/packages/vinxi-directives/transform.test.js +++ b/packages/vinxi-directives/transform.test.js @@ -2,10 +2,13 @@ import { prettyPrint } from "recast"; import { describe, expect, it } from "vitest"; import { parseAdvanced } from "./parse.js"; -import { shimExportsPlugin } from "./shim-exports.js"; -import { decorateExports } from "./transform.js"; -import { wrapExports } from "./wrap-exports.js"; -import { wrapExportsPlugin } from "./wrap-exports.js"; +import { + decorateExports, + decorateExportsPlugin, +} from "./plugins/decorate-exports.js"; +import { shimExportsPlugin } from "./plugins/shim-exports.js"; +import { wrapExports } from "./plugins/wrap-exports.js"; +import { wrapExportsPlugin } from "./plugins/wrap-exports.js"; const testFixtures = import.meta.glob("./fixtures/**/*.ts", { as: "raw", @@ -80,7 +83,6 @@ async function runTest(name, transform, f) { const expected = await testFixtures[ `./fixtures/${name}${f ? "." + f : ""}.snapshot.ts` ](); - console.log(await transform(code)); expect(js(await transform(code))).toEqual(js(expected)); }); } @@ -88,6 +90,9 @@ async function runTest(name, transform, f) { runTest("wrap-exports", (code) => transformSSR(code, wrapExportsPlugin)); runTest("wrap-exports-fn", (code) => transformSSR(code, wrapExportsPlugin)); runTest("shim-exports", (code) => transformSSR(code, shimExportsPlugin)); +runTest("decorate-exports", (code) => + transformSSR(code, decorateExportsPlugin), +); runTest("shim-exports-fn", (code) => transformSSR(code, shimExportsPlugin)); runTest("example-1", (code) => transformSSR(code, wrapExportsPlugin)); runTest("example-2", (code) => transformSSR(code, wrapExportsPlugin), "wrap"); diff --git a/packages/vinxi-directives/utils.js b/packages/vinxi-directives/utils.js new file mode 100644 index 00000000..2dc04065 --- /dev/null +++ b/packages/vinxi-directives/utils.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import { readFileSync, writeFileSync } from "fs"; + +import { parseAdvanced } from "./parse.js"; + +/** + * + * @param {{ resolve }} vite + * @param {string} specifier + * @param {string} parentURL + * @returns + */ +export async function resolveClientImport(vite, specifier, parentURL) { + const resolved = await vite.resolve(specifier, parentURL, { + skipSelf: true, + }); + + if (!resolved) { + throw new Error("Could not resolve " + specifier + " from " + parentURL); + } + + return { url: resolved.id }; +} + +export async function parseExportNamesInto(vite, ast, names, parentURL) { + for (let i = 0; i < ast.program.body.length; i++) { + const node = ast.program.body[i]; + switch (node.type) { + case "ExportAllDeclaration": + if (node.exported) { + addExportNames(names, node.exported); + continue; + } else { + const { url } = await resolveClientImport( + vite, + node.source.value, + parentURL, + ); + + const clientImportCode = readFileSync(url, "utf8"); + + const childAst = parseAdvanced(clientImportCode ?? ""); + + await parseExportNamesInto(vite, childAst.program.body, names, url); + continue; + } + case "ExportDefaultDeclaration": + names.push("default"); + continue; + case "ExportNamedDeclaration": + if (node.declaration) { + if (node.declaration.type === "VariableDeclaration") { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addExportNames(names, declarations[j].id); + } + } else { + addExportNames(names, node.declaration.id); + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + addExportNames(names, specifiers[j].exported); + } + } + continue; + } + } +} + +export function addLocalExportedNames(names, node) { + switch (node.type) { + case "Identifier": + names.set(node.name, node.name); + return; + case "ObjectPattern": + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(names, node.properties[i]); + return; + case "ArrayPattern": + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addLocalExportedNames(names, element); + } + return; + case "Property": + addLocalExportedNames(names, node.value); + return; + case "AssignmentPattern": + addLocalExportedNames(names, node.left); + return; + case "RestElement": + addLocalExportedNames(names, node.argument); + return; + case "ParenthesizedExpression": + addLocalExportedNames(names, node.expression); + return; + } +} + +export function addExportNames(names, node) { + switch (node.type) { + case "Identifier": + names.push(node.name); + return; + case "ObjectPattern": + for (let i = 0; i < node.properties.length; i++) + addExportNames(names, node.properties[i]); + return; + case "ArrayPattern": + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addExportNames(names, element); + } + return; + case "Property": + addExportNames(names, node.value); + return; + case "AssignmentPattern": + addExportNames(names, node.left); + return; + case "RestElement": + addExportNames(names, node.argument); + return; + case "ParenthesizedExpression": + addExportNames(names, node.expression); + return; + } +}