Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .github/actions/sandbox/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ runs:
SOURCEMETA_REGISTRY_SANDBOX_EDITION: ${{ inputs.edition }}
SOURCEMETA_REGISTRY_SANDBOX_CONFIGURATION: ${{ inputs.configuration }}
SOURCEMETA_REGISTRY_SANDBOX_PORT: ${{ inputs.port }}

# Registry tests
- name: Up Sandbox (${{ inputs.configuration }})
run: docker compose --file test/sandbox/compose.yaml up --detach --wait
shell: bash
Expand All @@ -51,6 +53,15 @@ runs:
env:
SANDBOX_CONFIGURATION: ${{ inputs.configuration }}
SANDBOX_PORT: ${{ inputs.port }}

# Clients
- name: Client - Node.js (${{ inputs.configuration }})
run: make -C clients/nodejs EDITION=${{ inputs.edition }}
if: inputs.configuration != 'empty'
shell: bash
env:
SANDBOX_PORT: ${{ inputs.port }}

- name: Down Sandbox (${{ inputs.configuration }})
run: docker compose --file test/sandbox/compose.yaml down
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ if(REGISTRY_SERVER)
endif()

sourcemeta_target_clang_format(SOURCES src/*.h src/*.cc test/*.cc contrib/*.cc)
sourcemeta_target_shellcheck(SOURCES test/*.sh docker/*.sh)
sourcemeta_target_shellcheck(SOURCES test/*.sh docker/*.sh clients/nodejs/test/*.sh)

set(SOURCEMETA_SCHEMAS "${PROJECT_SOURCE_DIR}/collections/sourcemeta/registry/schemas")
add_custom_target(jsonschema_fmt_test
Expand Down
2 changes: 2 additions & 0 deletions clients/nodejs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
*.tgz
13 changes: 13 additions & 0 deletions clients/nodejs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
SANDBOX_PORT ?= 8000
SANDBOX_URL ?= http://localhost:$(SANDBOX_PORT)

.PHONY: all
all: test

.PHONY: test
test:
./test/bundling-commonjs.sh $(SANDBOX_URL)
./test/bundling-modules.sh $(SANDBOX_URL)
./test/postinstall-auto.sh $(SANDBOX_URL)
./test/schema-add.sh $(SANDBOX_URL)
./test/nested-package.sh $(SANDBOX_URL)
102 changes: 102 additions & 0 deletions clients/nodejs/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const path = require('path');
const fs = require('fs');

/**
* Find the node_modules directory
* We're installed at node_modules/@sourcemeta/registry, so we can walk up
*/
function findNodeModules() {
let currentPath = __dirname;

while (currentPath !== path.dirname(currentPath)) {
const basename = path.basename(currentPath);

// Check if we're inside node_modules/@sourcemeta/registry
if (basename === 'registry') {
const parentPath = path.dirname(currentPath);
if (path.basename(parentPath) === '@sourcemeta') {
const grandParentPath = path.dirname(parentPath);
if (path.basename(grandParentPath) === 'node_modules') {
return grandParentPath;
}
}
}

currentPath = path.dirname(currentPath);
}

throw new Error('Could not find node_modules directory');
}

/**
* Find the consumer's package.json by looking at who is importing this module
* Uses the stack trace to find the actual caller's location
*/
function findConsumerPackageFromCaller() {
// Get the stack trace
const originalPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const stack = new Error().stack;
Error.prepareStackTrace = originalPrepareStackTrace;

// Find the first call site that's NOT in @sourcemeta/registry
for (const callSite of stack) {
const fileName = callSite.getFileName();
if (fileName && !fileName.includes('@sourcemeta/registry') && !fileName.includes('node:')) {
// Walk up from this file to find its package.json
let currentPath = path.dirname(fileName);
while (currentPath !== path.dirname(currentPath)) {
const packageJsonPath = path.join(currentPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return { name: packageJson.name, version: packageJson.version, path: currentPath };
}
currentPath = path.dirname(currentPath);
}
}
}

// Fallback to the old method
const nodeModulesDirectory = findNodeModules();
const consumerRoot = path.dirname(nodeModulesDirectory);
const packageJsonPath = path.join(consumerRoot, 'package.json');

if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return { name: packageJson.name, version: packageJson.version, path: consumerRoot };
}

throw new Error('Could not find consumer package.json');
}

// Cache schemas per consumer package
const schemasCache = new Map();

function getSchemas() {
const consumer = findConsumerPackageFromCaller();
const cacheKey = `${consumer.name}@${consumer.version}`;

if (!schemasCache.has(cacheKey)) {
const nodeModulesPath = path.join(consumer.path, 'node_modules');
const cacheIndexPath = path.join(
nodeModulesPath,
'.cache',
'@sourcemeta',
'registry',
consumer.name,
consumer.version,
'index.cjs'
);
schemasCache.set(cacheKey, require(cacheIndexPath));
}

return schemasCache.get(cacheKey);
}

module.exports = {
get schemas() {
return getSchemas();
}
};
117 changes: 117 additions & 0 deletions clients/nodejs/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Export schemas object that loads from the namespaced cache
// The cache is namespaced by consumer package name and version to avoid conflicts

import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname, join, basename } from 'path';
import { readFileSync } from 'fs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);

/**
* Find the consumer's package.json by looking at who is importing this module
* Uses the stack trace to find the actual caller's location
*/
function findConsumerPackageFromCaller() {
// Get the stack trace
const originalPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const stack = new Error().stack;
Error.prepareStackTrace = originalPrepareStackTrace;

// Find the first call site that's NOT in @sourcemeta/registry
for (const callSite of stack) {
const fileName = callSite.getFileName();
if (fileName && !fileName.includes('@sourcemeta/registry') && !fileName.startsWith('node:')) {
// Convert file:// URL to path if needed
let filePath = fileName;
if (filePath.startsWith('file://')) {
filePath = fileURLToPath(filePath);
}

// Walk up from this file to find its package.json
let currentPath = dirname(filePath);
while (currentPath !== dirname(currentPath)) {
const packageJsonPath = join(currentPath, 'package.json');
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
return { name: packageJson.name, version: packageJson.version, path: currentPath };
} catch (error) {
// Continue searching
}
currentPath = dirname(currentPath);
}
}
}

// Fallback to the old method
let currentPath = dirname(dirname(__dirname)); // Start from node_modules parent
while (currentPath !== dirname(currentPath)) {
const packageJsonPath = join(currentPath, 'package.json');
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
if (packageJson.name !== '@sourcemeta/registry') {
return { name: packageJson.name, version: packageJson.version, path: currentPath };
}
} catch (error) {
// Continue searching
}
currentPath = dirname(currentPath);
}

throw new Error('Could not find consumer package.json');
}

// Cache schemas per consumer package
const schemasCache = new Map();

function getSchemas() {
const consumer = findConsumerPackageFromCaller();
const cacheKey = `${consumer.name}@${consumer.version}`;

if (!schemasCache.has(cacheKey)) {
// Find the root node_modules directory by walking up from @sourcemeta/registry's location
// The cache is always in the root node_modules, not in the package's own node_modules
let rootNodeModules = dirname(__dirname); // node_modules/@sourcemeta
while (basename(rootNodeModules) !== 'node_modules' && rootNodeModules !== dirname(rootNodeModules)) {
rootNodeModules = dirname(rootNodeModules);
}

const cacheIndexPath = join(
rootNodeModules,
'.cache',
'@sourcemeta',
'registry',
consumer.name,
consumer.version,
'index.cjs'
);
schemasCache.set(cacheKey, require(cacheIndexPath));
}

return schemasCache.get(cacheKey);
}

export const schemas = new Proxy({}, {
get(target, prop) {
return getSchemas()[prop];
},
has(target, prop) {
return prop in getSchemas();
},
ownKeys() {
return Object.keys(getSchemas());
},
getOwnPropertyDescriptor(target, prop) {
const schemas = getSchemas();
if (prop in schemas) {
return {
enumerable: true,
configurable: true,
value: schemas[prop]
};
}
}
});
Loading