Skip to content

Commit 73981b1

Browse files
Tool to generate a Deno-compatible driver
1 parent 0dce027 commit 73981b1

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

packages/neo4j-driver-deno/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib/
2+
.vscode/

packages/neo4j-driver-deno/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Neo4j Driver for Deno (Experimental)
2+
3+
This folder contains a script which can auto-generate a version of
4+
`neo4j-driver-lite` that is fully compatible with Deno, including complete type
5+
information.
6+
7+
The resulting driver does not use any dependencies outside of the Deno standard
8+
library.
9+
10+
## Development instructions
11+
12+
To generate the driver, open a shell in this folder and run this command,
13+
specifying what version number you want the driver to identify as:
14+
15+
```
16+
deno run --allow-read --allow-write --allow-net ./generate.ts --version=4.4.0
17+
```
18+
19+
The script will:
20+
21+
1. Copy `neo4j-driver-lite` and the Neo4j packages it uses into a subfolder here
22+
called `lib`.
23+
1. Rewrite all imports to Deno-compatible versions
24+
1. Replace the "node channel" with the "browser channel"
25+
1. Test that the resulting driver can be imported by Deno and passes type checks
26+
27+
It is not necessary to do any other setup first; in particular, you don't need
28+
to install any of the Node packages or run any of the driver monorepo's other
29+
scripts. However, you do need to have Deno installed.
30+
31+
## Usage instructions
32+
33+
Once the driver is generated in the `lib` directory, you can import it and use
34+
it as you would use `neo4j-driver-lite` (refer to its documentation).
35+
36+
Here is an example:
37+
38+
```typescript
39+
import neo4j from "./lib/mod.ts";
40+
const URI = "bolt://localhost:7687";
41+
const driver = neo4j.driver(URI, neo4j.auth.basic("neo4j", "driverdemo"));
42+
const session = driver.session();
43+
44+
const results = await session.run("MATCH (n) RETURN n LIMIT 25");
45+
console.log(results.records);
46+
47+
await session.close();
48+
await driver.close();
49+
```
50+
51+
You can use `deno run --allow-net ...` or `deno repl` to run this example. If
52+
you don't have a running Neo4j instance, you can use
53+
`docker run --rm -p 7687:7687 -e NEO4J_AUTH=neo4j/driverdemo neo4j:4.4` to
54+
quickly spin one up.
55+
56+
## Tests
57+
58+
It is not yet possible to run the test suite with this driver. Contributions to
59+
make that possible would be welcome.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/**
2+
* Auto-generate a version of the Neo4j "lite" JavaScript driver that works with Deno.
3+
* After it has been generated, this will load the new driver to test that it can
4+
* be initialized and that its typing is correct.
5+
*
6+
* See this folder's README.md for more details.
7+
*
8+
* Note: another approach would be to make the Deno version the primary version
9+
* and use DNT (https://github.com/denoland/dnt) to generate the NodeJS version,
10+
* but that seems too disruptive for now, and DNT is a new tool.
11+
*/
12+
13+
import * as log from "https://deno.land/std@0.119.0/log/mod.ts";
14+
import { parse } from "https://deno.land/std@0.119.0/flags/mod.ts";
15+
import { ensureDir } from "https://deno.land/std@0.119.0/fs/mod.ts";
16+
import { join, relative } from "https://deno.land/std@0.119.0/path/mod.ts";
17+
18+
const isDir = (path: string) => {
19+
try {
20+
const stat = Deno.statSync(path);
21+
return stat.isDirectory;
22+
} catch {
23+
return false;
24+
}
25+
};
26+
27+
////////////////////////////////////////////////////////////////////////////////
28+
// Parse arguments
29+
const parsedArgs = parse(Deno.args, {
30+
string: ["version"],
31+
boolean: ["transform"], // Pass --no-transform to disable
32+
default: { transform: true },
33+
unknown: (arg) => {
34+
throw new Error(`Unknown argument "${arg}"`);
35+
},
36+
});
37+
38+
// Should we rewrite imports or simply copy the files unmodified?
39+
// Copying without changes can be useful to later generate a diff of the transforms
40+
const doTransform = parsedArgs["transform"];
41+
const version = parsedArgs.version ?? "0.0.0dev";
42+
43+
////////////////////////////////////////////////////////////////////////////////
44+
// Clear out the destination folder
45+
const rootOutDir = "lib/";
46+
await ensureDir(rootOutDir); // Make sure it exists
47+
for await (const existingFile of Deno.readDir(rootOutDir)) {
48+
await Deno.remove(`${rootOutDir}${existingFile.name}`, { recursive: true });
49+
}
50+
51+
////////////////////////////////////////////////////////////////////////////////
52+
// Define our function that copies each file and transforms imports
53+
async function copyAndTransform(inDir: string, outDir: string) {
54+
await ensureDir(outDir); // Make sure the target directory exists
55+
56+
const relativeRoot = relative(outDir, rootOutDir) || "."; // relative path to rootOutDir
57+
const packageImportsMap = {
58+
'neo4j-driver-core': `${relativeRoot}/core/index.ts`,
59+
'neo4j-driver-bolt-connection': `${relativeRoot}/bolt-connection/index.js`,
60+
// Replace the 'buffer' npm package with the compatible implementation from the deno standard library
61+
'buffer': 'https://deno.land/std@0.119.0/node/buffer.ts', // or can use 'https://esm.sh/buffer@6.0.3'
62+
// Replace the 'string_decoder' npm package with the compatible implementation from the deno standard library
63+
'string_decoder': 'https://deno.land/std@0.119.0/node/string_decoder.ts', // or can use 'https://esm.sh/string_decoder@1.3.0'
64+
};
65+
66+
// Recursively copy files from inDir to outDir
67+
for await (const existingFile of Deno.readDir(inDir)) {
68+
const inPath = join(inDir, existingFile.name);
69+
const outPath = join(outDir, existingFile.name);
70+
// If this is a directory, handle it recursively:
71+
if (existingFile.isDirectory) {
72+
await copyAndTransform(inPath, outPath);
73+
continue;
74+
}
75+
// At this point, this is a file. Copy it to the destination and transform it if needed.
76+
log.info(`Generating ${outPath}`);
77+
let contents = await Deno.readTextFile(inPath);
78+
79+
// Transform: rewrite imports
80+
if (doTransform) {
81+
if (existingFile.name.endsWith(".ts")) {
82+
// Transform TypeScript imports:
83+
contents = contents.replaceAll(
84+
// Match an import or export statement, even if it has a '// comment' after it:
85+
/ from '(\.[\w\/\.\-]+)'( \/\/.*)?$/gm,
86+
(_x, origPath) => {
87+
const newPath = isDir(`${inDir}/${origPath}`)
88+
? `${origPath}/index.ts`
89+
: `${origPath}.ts`;
90+
return ` from '${newPath}'`;
91+
},
92+
);
93+
94+
// Special fix. Replace:
95+
// import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection'
96+
// With:
97+
// // @deno-types=../../bolt-connection/types
98+
// import { DirectConnectionProvider, RoutingConnectionProvider } from '../../bolt-connection/index.js'
99+
contents = contents.replace(
100+
/import {([^}]*)} from \'neo4j-driver-bolt-connection\'/,
101+
`// @deno-types=${relativeRoot}/bolt-connection/types/index.d.ts\n` +
102+
`import {$1} from '${relativeRoot}/bolt-connection/index.js'`,
103+
);
104+
} else if (existingFile.name.endsWith(".js")) {
105+
106+
// transform .js file imports in bolt-connection:
107+
contents = contents.replaceAll(
108+
/ from '(\.[\w\/\.\-]+)'$/gm,
109+
(_x, origPath) => {
110+
const newPath = isDir(`${inDir}/${origPath}`)
111+
? `${origPath}/index.js`
112+
: `${origPath}.js`;
113+
return ` from '${newPath}'`;
114+
},
115+
);
116+
117+
}
118+
119+
// Transforms which apply to both .js and .ts files, and which must come after the above transforms:
120+
if (
121+
existingFile.name.endsWith(".ts") || existingFile.name.endsWith(".js")
122+
) {
123+
for (const [nodePackage, newImportUrl] of Object.entries(packageImportsMap)) {
124+
// Rewrite imports that use a Node.js package name (absolute imports):
125+
contents = contents.replaceAll(
126+
new RegExp(` from '${nodePackage}'$`, "gm"),
127+
` from '${newImportUrl}'`,
128+
);
129+
}
130+
}
131+
132+
// Special fix for bolt-connection/channel/index.js
133+
// Replace the "node channel" with the "browser channel", since Deno supports browser APIs
134+
if (inPath.endsWith("channel/index.js")) {
135+
contents = contents.replace(
136+
`export * from './node/index.js'`,
137+
`export * from './browser/index.js'`,
138+
);
139+
}
140+
141+
}
142+
143+
await Deno.writeTextFile(outPath, contents);
144+
}
145+
}
146+
147+
////////////////////////////////////////////////////////////////////////////////
148+
// Now generate the Deno driver
149+
150+
await copyAndTransform("../core/src", join(rootOutDir, "core"));
151+
await copyAndTransform(
152+
"../bolt-connection/src",
153+
join(rootOutDir, "bolt-connection"),
154+
);
155+
await copyAndTransform(
156+
"../bolt-connection/types",
157+
join(rootOutDir, "bolt-connection", "types"),
158+
);
159+
await copyAndTransform("../neo4j-driver-lite/src", rootOutDir);
160+
// Deno convention is to use "mod.ts" not "index.ts", so let's do that at least for the main/root import:
161+
await Deno.rename(join(rootOutDir, "index.ts"), join(rootOutDir, "mod.ts"))
162+
await Deno.writeTextFile(
163+
join(rootOutDir, "version.ts"),
164+
`export default "${version}" // Specified using --version when running generate.ts\n`,
165+
);
166+
167+
////////////////////////////////////////////////////////////////////////////////
168+
// Warnings show up at the end
169+
if (!doTransform) {
170+
log.warning("Transform step was skipped.");
171+
}
172+
if (!parsedArgs.version) {
173+
log.warning(
174+
"No version specified. Specify a version like this: --version=4.4.0",
175+
);
176+
}
177+
178+
////////////////////////////////////////////////////////////////////////////////
179+
// Now test the driver
180+
log.info("Testing the new driver (type checks only)");
181+
const importPath = "./" + relative(".", join(rootOutDir, "mod.ts")); // This is just ${rootOutDir}/index.ts but forced to start with "./"
182+
await import(importPath);
183+
log.info('Driver created and validated!');

0 commit comments

Comments
 (0)