Skip to content

Commit

Permalink
(fix): decorate exports use parse-advanced, fixes #36
Browse files Browse the repository at this point in the history
  • Loading branch information
nksaraf committed Dec 27, 2023
1 parent efc56d5 commit 45627be
Show file tree
Hide file tree
Showing 9 changed files with 538 additions and 630 deletions.
113 changes: 113 additions & 0 deletions packages/vinxi-directives/fixtures/decorate-exports.snapshot.ts
Original file line number Diff line number Diff line change
@@ -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<Settings> => {
return getDataAtPath("solid/settings");
}, "settings");

export const getMenuMain = cache(async (): Promise<MenuItem[]> => {
return getDataAtPath("solid/menu/main");
}, "menu:main");

export const getMenuFooter = cache(async (): Promise<MenuItem[]> => {
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");
107 changes: 107 additions & 0 deletions packages/vinxi-directives/fixtures/decorate-exports.ts
Original file line number Diff line number Diff line change
@@ -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<Settings> => {
return getDataAtPath("solid/settings");
}, "settings");

export const getMenuMain = cache(async (): Promise<MenuItem[]> => {
return getDataAtPath("solid/menu/main");
}, "menu:main");

export const getMenuFooter = cache(async (): Promise<MenuItem[]> => {
return getDataAtPath("solid/menu/footer");
}, "menu:footer");
9 changes: 6 additions & 3 deletions packages/vinxi-directives/index.js
Original file line number Diff line number Diff line change
@@ -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";
161 changes: 161 additions & 0 deletions packages/vinxi-directives/plugins/decorate-exports.js
Original file line number Diff line number Diff line change
@@ -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;
},
};
}
Loading

0 comments on commit 45627be

Please sign in to comment.