Skip to content

Proposal: A tool for generating a deno-optimized version of the driver #830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/neo4j-driver-deno/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib/
.vscode/
59 changes: 59 additions & 0 deletions packages/neo4j-driver-deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Neo4j Driver for Deno (Experimental)

This folder contains a script which can auto-generate a version of
`neo4j-driver-lite` that is fully compatible with Deno, including complete type
information.

The resulting driver does not use any dependencies outside of the Deno standard
library.

## Development instructions

To generate the driver, open a shell in this folder and run this command,
specifying what version number you want the driver to identify as:

```
deno run --allow-read --allow-write --allow-net ./generate.ts --version=4.4.0
```

The script will:

1. Copy `neo4j-driver-lite` and the Neo4j packages it uses into a subfolder here
called `lib`.
1. Rewrite all imports to Deno-compatible versions
1. Replace the "node channel" with the "browser channel"
1. Test that the resulting driver can be imported by Deno and passes type checks

It is not necessary to do any other setup first; in particular, you don't need
to install any of the Node packages or run any of the driver monorepo's other
scripts. However, you do need to have Deno installed.

## Usage instructions

Once the driver is generated in the `lib` directory, you can import it and use
it as you would use `neo4j-driver-lite` (refer to its documentation).

Here is an example:

```typescript
import neo4j from "./lib/mod.ts";
const URI = "bolt://localhost:7687";
const driver = neo4j.driver(URI, neo4j.auth.basic("neo4j", "driverdemo"));
const session = driver.session();

const results = await session.run("MATCH (n) RETURN n LIMIT 25");
console.log(results.records);

await session.close();
await driver.close();
```

You can use `deno run --allow-net ...` or `deno repl` to run this example. If
you don't have a running Neo4j instance, you can use
`docker run --rm -p 7687:7687 -e NEO4J_AUTH=neo4j/driverdemo neo4j:4.4` to
quickly spin one up.

## Tests

It is not yet possible to run the test suite with this driver. Contributions to
make that possible would be welcome.
183 changes: 183 additions & 0 deletions packages/neo4j-driver-deno/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Auto-generate a version of the Neo4j "lite" JavaScript driver that works with Deno.
* After it has been generated, this will load the new driver to test that it can
* be initialized and that its typing is correct.
*
* See this folder's README.md for more details.
*
* Note: another approach would be to make the Deno version the primary version
* and use DNT (https://github.com/denoland/dnt) to generate the NodeJS version,
* but that seems too disruptive for now, and DNT is a new tool.
*/

import * as log from "https://deno.land/[email protected]/log/mod.ts";
import { parse } from "https://deno.land/[email protected]/flags/mod.ts";
import { ensureDir } from "https://deno.land/[email protected]/fs/mod.ts";
import { join, relative } from "https://deno.land/[email protected]/path/mod.ts";

const isDir = (path: string) => {
try {
const stat = Deno.statSync(path);
return stat.isDirectory;
} catch {
return false;
}
};

////////////////////////////////////////////////////////////////////////////////
// Parse arguments
const parsedArgs = parse(Deno.args, {
string: ["version"],
boolean: ["transform"], // Pass --no-transform to disable
default: { transform: true },
unknown: (arg) => {
throw new Error(`Unknown argument "${arg}"`);
},
});

// Should we rewrite imports or simply copy the files unmodified?
// Copying without changes can be useful to later generate a diff of the transforms
const doTransform = parsedArgs["transform"];
const version = parsedArgs.version ?? "0.0.0dev";

////////////////////////////////////////////////////////////////////////////////
// Clear out the destination folder
const rootOutDir = "lib/";
await ensureDir(rootOutDir); // Make sure it exists
for await (const existingFile of Deno.readDir(rootOutDir)) {
await Deno.remove(`${rootOutDir}${existingFile.name}`, { recursive: true });
}

////////////////////////////////////////////////////////////////////////////////
// Define our function that copies each file and transforms imports
async function copyAndTransform(inDir: string, outDir: string) {
await ensureDir(outDir); // Make sure the target directory exists

const relativeRoot = relative(outDir, rootOutDir) || "."; // relative path to rootOutDir
const packageImportsMap = {
'neo4j-driver-core': `${relativeRoot}/core/index.ts`,
'neo4j-driver-bolt-connection': `${relativeRoot}/bolt-connection/index.js`,
// Replace the 'buffer' npm package with the compatible implementation from the deno standard library
'buffer': 'https://deno.land/[email protected]/node/buffer.ts', // or can use 'https://esm.sh/[email protected]'
// Replace the 'string_decoder' npm package with the compatible implementation from the deno standard library
'string_decoder': 'https://deno.land/[email protected]/node/string_decoder.ts', // or can use 'https://esm.sh/[email protected]'
};

// Recursively copy files from inDir to outDir
for await (const existingFile of Deno.readDir(inDir)) {
const inPath = join(inDir, existingFile.name);
const outPath = join(outDir, existingFile.name);
// If this is a directory, handle it recursively:
if (existingFile.isDirectory) {
await copyAndTransform(inPath, outPath);
continue;
}
// At this point, this is a file. Copy it to the destination and transform it if needed.
log.info(`Generating ${outPath}`);
let contents = await Deno.readTextFile(inPath);

// Transform: rewrite imports
if (doTransform) {
if (existingFile.name.endsWith(".ts")) {
// Transform TypeScript imports:
contents = contents.replaceAll(
// Match an import or export statement, even if it has a '// comment' after it:
/ from '(\.[\w\/\.\-]+)'( \/\/.*)?$/gm,
(_x, origPath) => {
const newPath = isDir(`${inDir}/${origPath}`)
? `${origPath}/index.ts`
: `${origPath}.ts`;
return ` from '${newPath}'`;
},
);

// Special fix. Replace:
// import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection'
// With:
// // @deno-types=../../bolt-connection/types
// import { DirectConnectionProvider, RoutingConnectionProvider } from '../../bolt-connection/index.js'
contents = contents.replace(
/import {([^}]*)} from \'neo4j-driver-bolt-connection\'/,
`// @deno-types=${relativeRoot}/bolt-connection/types/index.d.ts\n` +
`import {$1} from '${relativeRoot}/bolt-connection/index.js'`,
);
} else if (existingFile.name.endsWith(".js")) {

// transform .js file imports in bolt-connection:
contents = contents.replaceAll(
/ from '(\.[\w\/\.\-]+)'$/gm,
(_x, origPath) => {
const newPath = isDir(`${inDir}/${origPath}`)
? `${origPath}/index.js`
: `${origPath}.js`;
return ` from '${newPath}'`;
},
);

}

// Transforms which apply to both .js and .ts files, and which must come after the above transforms:
if (
existingFile.name.endsWith(".ts") || existingFile.name.endsWith(".js")
) {
for (const [nodePackage, newImportUrl] of Object.entries(packageImportsMap)) {
// Rewrite imports that use a Node.js package name (absolute imports):
contents = contents.replaceAll(
new RegExp(` from '${nodePackage}'$`, "gm"),
` from '${newImportUrl}'`,
);
}
}

// Special fix for bolt-connection/channel/index.js
// Replace the "node channel" with the "browser channel", since Deno supports browser APIs
if (inPath.endsWith("channel/index.js")) {
contents = contents.replace(
`export * from './node/index.js'`,
`export * from './browser/index.js'`,
);
}

}

await Deno.writeTextFile(outPath, contents);
}
}

////////////////////////////////////////////////////////////////////////////////
// Now generate the Deno driver

await copyAndTransform("../core/src", join(rootOutDir, "core"));
await copyAndTransform(
"../bolt-connection/src",
join(rootOutDir, "bolt-connection"),
);
await copyAndTransform(
"../bolt-connection/types",
join(rootOutDir, "bolt-connection", "types"),
);
await copyAndTransform("../neo4j-driver-lite/src", rootOutDir);
// Deno convention is to use "mod.ts" not "index.ts", so let's do that at least for the main/root import:
await Deno.rename(join(rootOutDir, "index.ts"), join(rootOutDir, "mod.ts"))
await Deno.writeTextFile(
join(rootOutDir, "version.ts"),
`export default "${version}" // Specified using --version when running generate.ts\n`,
);

////////////////////////////////////////////////////////////////////////////////
// Warnings show up at the end
if (!doTransform) {
log.warning("Transform step was skipped.");
}
if (!parsedArgs.version) {
log.warning(
"No version specified. Specify a version like this: --version=4.4.0",
);
}

////////////////////////////////////////////////////////////////////////////////
// Now test the driver
log.info("Testing the new driver (type checks only)");
const importPath = "./" + relative(".", join(rootOutDir, "mod.ts")); // This is just ${rootOutDir}/index.ts but forced to start with "./"
await import(importPath);
log.info('Driver created and validated!');