Skip to content

Commit

Permalink
Merge pull request #105 from dancrumb/feat/#103
Browse files Browse the repository at this point in the history
feat: 🎸 handle environment variable access
  • Loading branch information
garronej authored Jun 13, 2023
2 parents b06bdd2 + 8bc4970 commit 5848e02
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 114 deletions.
32 changes: 32 additions & 0 deletions src/lib/builtins/__dirname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const DIRNAME_IS_USED = /(?:^|[\s\(\);=><{}\[\]\/:?,])__dirname(?:$|[^a-zA-Z0-9$_-])/;

export const test = (sourceCode: string) => DIRNAME_IS_USED.test(sourceCode);

export const modification = [
`const __dirname = (() => {`,
` const { url: urlStr } = import.meta;`,
` const url = new URL(urlStr);`,
` const __filename = (url.protocol === "file:" ? url.pathname : urlStr)`,
` .replace(/[/][^/]*$/, '');`,
``,
` const isWindows = (() => {`,
``,
` let NATIVE_OS: typeof Deno.build.os = "linux";`,
` // eslint-disable-next-line @typescript-eslint/no-explicit-any`,
` const navigator = (globalThis as any).navigator;`,
` if (globalThis.Deno != null) {`,
` NATIVE_OS = Deno.build.os;`,
` } else if (navigator?.appVersion?.includes?.("Win") ?? false) {`,
` NATIVE_OS = "windows";`,
` }`,
``,
` return NATIVE_OS == "windows";`,
``,
` })();`,
``,
` return isWindows ?`,
` __filename.split("/").join("\\\\").substring(1) :`,
` __filename;`,
`})();`,
``
];
31 changes: 31 additions & 0 deletions src/lib/builtins/__filename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const FILENAME_IS_USED = /(?:^|[\s\(\);=><{}\[\]\/:?,])__filename(?:$|[^a-zA-Z0-9$_-])/;

export const test = (sourceCode: string) => FILENAME_IS_USED.test(sourceCode);

export const modification = [
`const __filename = (() => {`,
` const { url: urlStr } = import.meta;`,
` const url = new URL(urlStr);`,
` const __filename = (url.protocol === "file:" ? url.pathname : urlStr);`,
``,
` const isWindows = (() => {`,
``,
` let NATIVE_OS: typeof Deno.build.os = "linux";`,
` // eslint-disable-next-line @typescript-eslint/no-explicit-any`,
` const navigator = (globalThis as any).navigator;`,
` if (globalThis.Deno != null) {`,
` NATIVE_OS = Deno.build.os;`,
` } else if (navigator?.appVersion?.includes?.("Win") ?? false) {`,
` NATIVE_OS = "windows";`,
` }`,
``,
` return NATIVE_OS == "windows";`,
``,
` })();`,
``,
` return isWindows ?`,
` __filename.split("/").join("\\\\").substring(1) :`,
` __filename;`,
`})();`,
``
];
6 changes: 6 additions & 0 deletions src/lib/builtins/buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const BUFFER_IS_USED = /(?:^|[\s\(\);=><{}\[\]\/:?,])Buffer(?:$|[^a-zA-Z0-9$_-])/;
const BUFFER_IS_IMPORTED = /import\s*{[^}]*Buffer[^}]*}\s*from\s*["'][^"']+["']/;

export const test = (sourceCode: string) => BUFFER_IS_USED.test(sourceCode) && !BUFFER_IS_IMPORTED.test(sourceCode);

export const modification = [`import { Buffer } from "buffer";`];
15 changes: 15 additions & 0 deletions src/lib/builtins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as __dirname from "./__dirname";
import * as __filename from "./__filename";
import * as buffer from "./buffer";

/**
* This is how we handle Node builtins
*
* Each module in this directory should export two functions:
* - test: (sourceCode: string) => boolean returns true if the source code needs to be modified because it refers to a Node builtin
* - modification: string[] the lines of code to prepend to the source code
*/

const builtins = [__filename, __dirname, buffer];

export default builtins;
153 changes: 41 additions & 112 deletions src/lib/denoifySingleFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { replaceAsync } from "../tools/replaceAsync";
import type { denoifyImportExportStatementFactory } from "./denoifyImportExportStatement";
import * as crypto from "crypto";
import builtins from "./builtins/index";

/**
* Remove any lines containing and following // @denoify-line-ignore
*/
const IGNORE_LINE_COMMENT = /^\/\/\s+@denoify-line-ignore/;
function dealWithDenoifyLineIgnoreSpecialComment(sourceCode: string): string {
let previousLineHadIgnoreComment = false;
return sourceCode
.split("\n")
.filter(line => {
const thisLineHasIgnoreComment = IGNORE_LINE_COMMENT.test(line);
const skipThisLine = thisLineHasIgnoreComment || previousLineHadIgnoreComment;

if (previousLineHadIgnoreComment) {
previousLineHadIgnoreComment = false;
}
if (thisLineHasIgnoreComment) {
previousLineHadIgnoreComment = thisLineHasIgnoreComment;
}
return !skipThisLine;
})
.join("\n");
}

export function denoifySingleFileFactory(params: {} & ReturnType<typeof denoifyImportExportStatementFactory>) {
const { denoifyImportExportStatement } = params;
Expand All @@ -9,96 +33,17 @@ export function denoifySingleFileFactory(params: {} & ReturnType<typeof denoifyI
async function denoifySingleFile(params: { dirPath: string; sourceCode: string }): Promise<string> {
const { dirPath, sourceCode } = params;

let modifiedSourceCode = sourceCode;

modifiedSourceCode = (function dealWithDenoifyLineIgnoreSpecialComment(sourceCode: string): string {
let split = sourceCode.split("\n");

split = split.map((line, i) => (i === split.length - 1 ? line : `${line}\n`));

const outSplit = [];
// Handle ignore comments
let modifiedSourceCode = dealWithDenoifyLineIgnoreSpecialComment(sourceCode);

for (let i = 0; i < split.length; i++) {
const line = split[i];

if (!line.startsWith("// @denoify-line-ignore")) {
outSplit.push(line);
continue;
}

i++;
// Add support for Node builtins
for (const builtin of builtins) {
if (builtin.test(modifiedSourceCode)) {
modifiedSourceCode = [...builtin.modification, modifiedSourceCode].join("\n");
}

return outSplit.join("");
})(modifiedSourceCode);

if (usesBuiltIn("__filename", sourceCode)) {
modifiedSourceCode = [
`const __filename = (() => {`,
` const { url: urlStr } = import.meta;`,
` const url = new URL(urlStr);`,
` const __filename = (url.protocol === "file:" ? url.pathname : urlStr);`,
``,
` const isWindows = (() => {`,
``,
` let NATIVE_OS: typeof Deno.build.os = "linux";`,
` // eslint-disable-next-line @typescript-eslint/no-explicit-any`,
` const navigator = (globalThis as any).navigator;`,
` if (globalThis.Deno != null) {`,
` NATIVE_OS = Deno.build.os;`,
` } else if (navigator?.appVersion?.includes?.("Win") ?? false) {`,
` NATIVE_OS = "windows";`,
` }`,
``,
` return NATIVE_OS == "windows";`,
``,
` })();`,
``,
` return isWindows ?`,
` __filename.split("/").join("\\\\").substring(1) :`,
` __filename;`,
`})();`,
``,
modifiedSourceCode
].join("\n");
}

if (usesBuiltIn("__dirname", sourceCode)) {
modifiedSourceCode = [
`const __dirname = (() => {`,
` const { url: urlStr } = import.meta;`,
` const url = new URL(urlStr);`,
` const __filename = (url.protocol === "file:" ? url.pathname : urlStr)`,
` .replace(/[/][^/]*$/, '');`,
``,
` const isWindows = (() => {`,
``,
` let NATIVE_OS: typeof Deno.build.os = "linux";`,
` // eslint-disable-next-line @typescript-eslint/no-explicit-any`,
` const navigator = (globalThis as any).navigator;`,
` if (globalThis.Deno != null) {`,
` NATIVE_OS = Deno.build.os;`,
` } else if (navigator?.appVersion?.includes?.("Win") ?? false) {`,
` NATIVE_OS = "windows";`,
` }`,
``,
` return NATIVE_OS == "windows";`,
``,
` })();`,
``,
` return isWindows ?`,
` __filename.split("/").join("\\\\").substring(1) :`,
` __filename;`,
`})();`,
``,
modifiedSourceCode
].join("\n");
}

if (usesBuiltIn("Buffer", sourceCode)) {
modifiedSourceCode = [`import { Buffer } from "buffer";`, modifiedSourceCode].join("\n");
}

// Cleanup import/export statements
const denoifiedImportExportStatementByHash = new Map<string, string>();

for (const quoteSymbol of [`"`, `'`]) {
Expand Down Expand Up @@ -130,6 +75,16 @@ export function denoifySingleFileFactory(params: {} & ReturnType<typeof denoifyI
}
}

// Handle environment variable access
modifiedSourceCode = modifiedSourceCode.replaceAll(
/=\s+process.env(?:\.(?<varDot>\w+)|\[(?<q>['"])(?<varBracket>\w+)\k<q>\])/g,
`= Deno.env.get('$<varDot>$<varBracket>')`
);
modifiedSourceCode = modifiedSourceCode.replaceAll(
/process.env(?:\.(?<varDot>\w+)|\[(?<q>['"])(?<varBracket>\w+)\k<q>\])\s+=\s+(?<val>[^;]+)/g,
`Deno.env.set('$<varDot>$<varBracket>', $<val>)`
);

for (const [hash, denoifiedImportExportStatement] of denoifiedImportExportStatementByHash) {
modifiedSourceCode = modifiedSourceCode.replace(new RegExp(hash, "g"), denoifiedImportExportStatement);
}
Expand All @@ -139,29 +94,3 @@ export function denoifySingleFileFactory(params: {} & ReturnType<typeof denoifyI

return { denoifySingleFile };
}

/*
TODO: This is really at proof of concept stage.
In the current implementation if any of those keyword appear in the source
regardless of the context (including comments) the polyfills will be included.
For now this implementation will do the trick, priority goes to polyfilling the
node's builtins but this need to be improved later on.
*/
function usesBuiltIn(builtIn: "__filename" | "__dirname" | "Buffer", sourceCode: string): boolean {
switch (builtIn) {
case "Buffer": {
//We should return false for example
//if we have an import from the browserify polyfill
//e.g.: import { Buffer } from "buffer";
if (!!sourceCode.match(/import\s*{[^}]*Buffer[^}]*}\s*from\s*["'][^"']+["']/)) {
return false;
}
}
case "__dirname":
case "__filename":
return new RegExp(`(?:^|[\\s\\(\\);=><{}\\[\\]\\/:?,])${builtIn}(?:$|[^a-zA-Z0-9$_-])`).test(sourceCode);
}
}
54 changes: 54 additions & 0 deletions test/denoifySingleFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,60 @@ Buffer_name
"dirPath": ""
});

expect(modifiedSourceCode).toBe(expected);
});
it("should access environment variables correctly", async () => {
const sourceCode = `const foo = process.env.FOO\nconst bar = process.env['BAR'];`;

const expected = `const foo = Deno.env.get('FOO')\nconst bar = Deno.env.get('BAR');`;

const { denoifySingleFile } = denoifySingleFileFactory({
"denoifyImportExportStatement": () => {
tsafeAssert(false);
}
});

const modifiedSourceCode = await denoifySingleFile({
sourceCode,
"dirPath": ""
});

expect(modifiedSourceCode).toBe(expected);
});
it("should set environment variables correctly", async () => {
const sourceCode = `process.env.FOO = 'foo';\nprocess.env['BAR'] = 22;`;

const expected = `Deno.env.set('FOO', 'foo');\nDeno.env.set('BAR', 22);`;

const { denoifySingleFile } = denoifySingleFileFactory({
"denoifyImportExportStatement": () => {
tsafeAssert(false);
}
});

const modifiedSourceCode = await denoifySingleFile({
sourceCode,
"dirPath": ""
});

expect(modifiedSourceCode).toBe(expected);
});
it("should update environment variables correctly", async () => {
const sourceCode = `process.env.FOO = process.env['BAR']+1;\nprocess.env['BAR'] = process.env.FOO + 'bar';`;

const expected = `Deno.env.set('FOO', Deno.env.get('BAR')+1);\nDeno.env.set('BAR', Deno.env.get('FOO') + 'bar');`;

const { denoifySingleFile } = denoifySingleFileFactory({
"denoifyImportExportStatement": () => {
tsafeAssert(false);
}
});

const modifiedSourceCode = await denoifySingleFile({
sourceCode,
"dirPath": ""
});

expect(modifiedSourceCode).toBe(expected);
});
});
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2018",
"lib": ["es2018", "ES2019.Object"],
"target": "ES2021",
"declaration": true,
"outDir": "./dist",
"sourceMap": true,
Expand Down

0 comments on commit 5848e02

Please sign in to comment.