Skip to content

Commit 59db2c9

Browse files
committed
[WIP] Introduce a Node.js client helper module
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent 99e69d2 commit 59db2c9

File tree

14 files changed

+1296
-1
lines changed

14 files changed

+1296
-1
lines changed

.github/actions/sandbox/action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ runs:
3030
SOURCEMETA_REGISTRY_SANDBOX_EDITION: ${{ inputs.edition }}
3131
SOURCEMETA_REGISTRY_SANDBOX_CONFIGURATION: ${{ inputs.configuration }}
3232
SOURCEMETA_REGISTRY_SANDBOX_PORT: ${{ inputs.port }}
33+
34+
# Registry tests
3335
- name: Up Sandbox (${{ inputs.configuration }})
3436
run: docker compose --file test/sandbox/compose.yaml up --detach --wait
3537
shell: bash
@@ -51,6 +53,15 @@ runs:
5153
env:
5254
SANDBOX_CONFIGURATION: ${{ inputs.configuration }}
5355
SANDBOX_PORT: ${{ inputs.port }}
56+
57+
# Clients
58+
- name: Client - Node.js (${{ inputs.configuration }})
59+
run: make -C clients/nodejs EDITION=${{ inputs.edition }}
60+
if: inputs.configuration != 'empty'
61+
shell: bash
62+
env:
63+
SANDBOX_PORT: ${{ inputs.port }}
64+
5465
- name: Down Sandbox (${{ inputs.configuration }})
5566
run: docker compose --file test/sandbox/compose.yaml down
5667
shell: bash

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ if(REGISTRY_SERVER)
9191
endif()
9292

9393
sourcemeta_target_clang_format(SOURCES src/*.h src/*.cc test/*.cc contrib/*.cc)
94-
sourcemeta_target_shellcheck(SOURCES test/*.sh docker/*.sh)
94+
sourcemeta_target_shellcheck(SOURCES test/*.sh docker/*.sh clients/nodejs/test/*.sh)
9595

9696
set(SOURCEMETA_SCHEMAS "${PROJECT_SOURCE_DIR}/collections/sourcemeta/registry/schemas")
9797
add_custom_target(jsonschema_fmt_test

clients/nodejs/.gitignore

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

clients/nodejs/Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
SANDBOX_PORT ?= 8000
2+
SANDBOX_URL ?= http://localhost:$(SANDBOX_PORT)
3+
4+
.PHONY: all
5+
all: test
6+
7+
.PHONY: test
8+
test:
9+
./test/bundling-commonjs.sh $(SANDBOX_URL)
10+
./test/bundling-modules.sh $(SANDBOX_URL)
11+
./test/postinstall-auto.sh $(SANDBOX_URL)
12+
./test/schema-add.sh $(SANDBOX_URL)
13+
./test/nested-package.sh $(SANDBOX_URL)

clients/nodejs/index.cjs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
const fs = require('fs');
5+
6+
/**
7+
* Find the node_modules directory
8+
* We're installed at node_modules/@sourcemeta/registry, so we can walk up
9+
*/
10+
function findNodeModules() {
11+
let currentPath = __dirname;
12+
13+
while (currentPath !== path.dirname(currentPath)) {
14+
const basename = path.basename(currentPath);
15+
16+
// Check if we're inside node_modules/@sourcemeta/registry
17+
if (basename === 'registry') {
18+
const parentPath = path.dirname(currentPath);
19+
if (path.basename(parentPath) === '@sourcemeta') {
20+
const grandParentPath = path.dirname(parentPath);
21+
if (path.basename(grandParentPath) === 'node_modules') {
22+
return grandParentPath;
23+
}
24+
}
25+
}
26+
27+
currentPath = path.dirname(currentPath);
28+
}
29+
30+
throw new Error('Could not find node_modules directory');
31+
}
32+
33+
/**
34+
* Find the consumer's package.json by looking at who is importing this module
35+
* Uses the stack trace to find the actual caller's location
36+
*/
37+
function findConsumerPackageFromCaller() {
38+
// Get the stack trace
39+
const originalPrepareStackTrace = Error.prepareStackTrace;
40+
Error.prepareStackTrace = (_, stack) => stack;
41+
const stack = new Error().stack;
42+
Error.prepareStackTrace = originalPrepareStackTrace;
43+
44+
// Find the first call site that's NOT in @sourcemeta/registry
45+
for (const callSite of stack) {
46+
const fileName = callSite.getFileName();
47+
if (fileName && !fileName.includes('@sourcemeta/registry') && !fileName.includes('node:')) {
48+
// Walk up from this file to find its package.json
49+
let currentPath = path.dirname(fileName);
50+
while (currentPath !== path.dirname(currentPath)) {
51+
const packageJsonPath = path.join(currentPath, 'package.json');
52+
if (fs.existsSync(packageJsonPath)) {
53+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
54+
return { name: packageJson.name, version: packageJson.version, path: currentPath };
55+
}
56+
currentPath = path.dirname(currentPath);
57+
}
58+
}
59+
}
60+
61+
// Fallback to the old method
62+
const nodeModulesDirectory = findNodeModules();
63+
const consumerRoot = path.dirname(nodeModulesDirectory);
64+
const packageJsonPath = path.join(consumerRoot, 'package.json');
65+
66+
if (fs.existsSync(packageJsonPath)) {
67+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
68+
return { name: packageJson.name, version: packageJson.version, path: consumerRoot };
69+
}
70+
71+
throw new Error('Could not find consumer package.json');
72+
}
73+
74+
// Cache schemas per consumer package
75+
const schemasCache = new Map();
76+
77+
function getSchemas() {
78+
const consumer = findConsumerPackageFromCaller();
79+
const cacheKey = `${consumer.name}@${consumer.version}`;
80+
81+
if (!schemasCache.has(cacheKey)) {
82+
const nodeModulesPath = path.join(consumer.path, 'node_modules');
83+
const cacheIndexPath = path.join(
84+
nodeModulesPath,
85+
'.cache',
86+
'@sourcemeta',
87+
'registry',
88+
consumer.name,
89+
consumer.version,
90+
'index.cjs'
91+
);
92+
schemasCache.set(cacheKey, require(cacheIndexPath));
93+
}
94+
95+
return schemasCache.get(cacheKey);
96+
}
97+
98+
module.exports = {
99+
get schemas() {
100+
return getSchemas();
101+
}
102+
};

clients/nodejs/index.mjs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Export schemas object that loads from the namespaced cache
2+
// The cache is namespaced by consumer package name and version to avoid conflicts
3+
4+
import { createRequire } from 'module';
5+
import { fileURLToPath } from 'url';
6+
import { dirname, join, basename } from 'path';
7+
import { readFileSync } from 'fs';
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = dirname(__filename);
11+
const require = createRequire(import.meta.url);
12+
13+
/**
14+
* Find the consumer's package.json by looking at who is importing this module
15+
* Uses the stack trace to find the actual caller's location
16+
*/
17+
function findConsumerPackageFromCaller() {
18+
// Get the stack trace
19+
const originalPrepareStackTrace = Error.prepareStackTrace;
20+
Error.prepareStackTrace = (_, stack) => stack;
21+
const stack = new Error().stack;
22+
Error.prepareStackTrace = originalPrepareStackTrace;
23+
24+
// Find the first call site that's NOT in @sourcemeta/registry
25+
for (const callSite of stack) {
26+
const fileName = callSite.getFileName();
27+
if (fileName && !fileName.includes('@sourcemeta/registry') && !fileName.startsWith('node:')) {
28+
// Convert file:// URL to path if needed
29+
let filePath = fileName;
30+
if (filePath.startsWith('file://')) {
31+
filePath = fileURLToPath(filePath);
32+
}
33+
34+
// Walk up from this file to find its package.json
35+
let currentPath = dirname(filePath);
36+
while (currentPath !== dirname(currentPath)) {
37+
const packageJsonPath = join(currentPath, 'package.json');
38+
try {
39+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
40+
return { name: packageJson.name, version: packageJson.version, path: currentPath };
41+
} catch (error) {
42+
// Continue searching
43+
}
44+
currentPath = dirname(currentPath);
45+
}
46+
}
47+
}
48+
49+
// Fallback to the old method
50+
let currentPath = dirname(dirname(__dirname)); // Start from node_modules parent
51+
while (currentPath !== dirname(currentPath)) {
52+
const packageJsonPath = join(currentPath, 'package.json');
53+
try {
54+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
55+
if (packageJson.name !== '@sourcemeta/registry') {
56+
return { name: packageJson.name, version: packageJson.version, path: currentPath };
57+
}
58+
} catch (error) {
59+
// Continue searching
60+
}
61+
currentPath = dirname(currentPath);
62+
}
63+
64+
throw new Error('Could not find consumer package.json');
65+
}
66+
67+
// Cache schemas per consumer package
68+
const schemasCache = new Map();
69+
70+
function getSchemas() {
71+
const consumer = findConsumerPackageFromCaller();
72+
const cacheKey = `${consumer.name}@${consumer.version}`;
73+
74+
if (!schemasCache.has(cacheKey)) {
75+
// Find the root node_modules directory by walking up from @sourcemeta/registry's location
76+
// The cache is always in the root node_modules, not in the package's own node_modules
77+
let rootNodeModules = dirname(__dirname); // node_modules/@sourcemeta
78+
while (basename(rootNodeModules) !== 'node_modules' && rootNodeModules !== dirname(rootNodeModules)) {
79+
rootNodeModules = dirname(rootNodeModules);
80+
}
81+
82+
const cacheIndexPath = join(
83+
rootNodeModules,
84+
'.cache',
85+
'@sourcemeta',
86+
'registry',
87+
consumer.name,
88+
consumer.version,
89+
'index.cjs'
90+
);
91+
schemasCache.set(cacheKey, require(cacheIndexPath));
92+
}
93+
94+
return schemasCache.get(cacheKey);
95+
}
96+
97+
export const schemas = new Proxy({}, {
98+
get(target, prop) {
99+
return getSchemas()[prop];
100+
},
101+
has(target, prop) {
102+
return prop in getSchemas();
103+
},
104+
ownKeys() {
105+
return Object.keys(getSchemas());
106+
},
107+
getOwnPropertyDescriptor(target, prop) {
108+
const schemas = getSchemas();
109+
if (prop in schemas) {
110+
return {
111+
enumerable: true,
112+
configurable: true,
113+
value: schemas[prop]
114+
};
115+
}
116+
}
117+
});

0 commit comments

Comments
 (0)