Skip to content
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
40 changes: 38 additions & 2 deletions examples/embed-objectql/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,9 @@
// shape you would ship in a `*.object.ts` to a full ObjectStack backend. One
// object model, two hosts; only the installed capability set differs.

import { realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ObjectQL } from '@objectstack/objectql/core';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { ObjectSchema, Field, type ServiceObject } from '@objectstack/spec/data';
Expand DownExpand Up@@ -56,8 +59,41 @@ export async function runEmbeddedEngine(): Promise<AccountRow[]> {
}) as Promise<AccountRow[]>;
}

// Allow `node`/`tsx`-style direct execution to print the result.
if (import.meta.url === `file://${process.argv[1]}`) {
// ─── entry guard ───────────────────────────────────────────────────────
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
// the template also skips the percent-encoding `pathToFileURL` applies — so that
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
// containing a character that needs encoding (a `#` in a parent directory name is
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
//
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
// `scripts/invoked-as.mjs` (`invokedAs`). Spelled out rather than imported because
// neither home is legally reachable from this file — the PR for #10269 carries the
// boundary measurement. ⚠️ Two predicates answering this question differently IS the
// defect this closes; change one, change all of them.
function isProcessEntry(): boolean {
const entryArg = process.argv[1];
if (!entryArg) return false; // `node --eval` / the REPL
const self = resolve(fileURLToPath(import.meta.url));
const entry = resolve(entryArg);
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
if (candidates.includes(self)) return true;
const realSelf = realOrSelf(self);
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
}

/** `realpathSync`, degrading to the input for a path that cannot be read. */
function realOrSelf(p: string): string {
try {
return realpathSync(p);
} catch {
return p;
}
}

if (isProcessEntry()) {
runEmbeddedEngine()
.then((rows) => {
// eslint-disable-next-line no-console
Expand Down
25 changes: 15 additions & 10 deletions packages/cli/src/utils/invocation.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,8 +30,11 @@
* budget is one small module and no side effects. Pulling `./format.js` for
* {@link CLI_NAME} would drag chalk, zod and `@objectstack/spec` into a shim
* whose whole job is to print one line and get out of the way, so the prefix is
* spelled locally and `invocation.cli-name-parity.test.ts` fails if the two
* spellings ever disagree.
* spelled locally and the parity case in `invocation.test.ts` (`INVOCATION_PREFIX`
* vs `CLI_NAME`) fails if the two spellings ever disagree. ⚠️ That case lives in
* `invocation.test.ts`, NOT in an `invocation.cli-name-parity.test.ts` — this
* header named the latter until #10269 and no such file is in the tree, so a
* reader grepping for it finds nothing and could read the parity as unguarded.
*/

import { realpathSync } from 'node:fs';
Expand DownExpand Up@@ -60,14 +63,16 @@ function realOrSelf(path: string): string {
* at — rather than a module someone imported?
*
* ⚠️ The obvious spelling of this predicate is the bug it guards against.
* #10086 measured the `invokedDirectly` guard across `scripts/` in ~8 spellings,
* all of them some form of `resolve(argv[1]) === fileURLToPath(import.meta.url)`,
* and EVERY one of them answers **false** when the script is reached through a
* symlink — because node resolves symlinks for the module graph but leaves
* `process.argv[1]` exactly as the caller typed it. A guard used the usual way
* ("only run when invoked directly") then makes its script silently inert: exit
* 0, no output. That is precisely the defect this module exists to remove, so
* reproducing it here would have been the same bug wearing the fix's clothes.
* #10086 measured the `invokedDirectly` guard across `scripts/` in ELEVEN distinct
* spellings over 33 files — the measurement, not the "~8" estimate this header
* carried until #10269 — and NINE of the eleven were wrong. The dominant family is
* some form of `resolve(argv[1]) === fileURLToPath(import.meta.url)`, and every
* member of it answers **false** when the script is reached through a symlink —
* because node resolves symlinks for the module graph but leaves `process.argv[1]`
* exactly as the caller typed it. A guard used the usual way ("only run when
* invoked directly") then makes its script silently inert: exit 0, no output. That
* is precisely the defect this module exists to remove, so reproducing it here
* would have been the same bug wearing the fix's clothes.
*
* Two things follow, and both are load-bearing:
*
Expand Down
40 changes: 38 additions & 2 deletions packages/core/examples/kernel-features-example.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,9 @@
* - Graceful shutdown
*/

import { realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ObjectKernel,
PluginMetadata,
Expand DownExpand Up@@ -300,8 +303,41 @@ async function main() {
console.log('\n✅ Shutdown complete!\n');
}

// Run the example
if (import.meta.url === `file://${process.argv[1]}`) {
// ─── entry guard ───────────────────────────────────────────────────────
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
// the template also skips the percent-encoding `pathToFileURL` applies — so that
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
// containing a character that needs encoding (a `#` in a parent directory name is
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
//
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
// `scripts/invoked-as.mjs` (`invokedAs`). Spelled out rather than imported because
// neither home is legally reachable from this file — the PR for #10269 carries the
// boundary measurement. ⚠️ Two predicates answering this question differently IS the
// defect this closes; change one, change all of them.
function isProcessEntry(): boolean {
const entryArg = process.argv[1];
if (!entryArg) return false; // `node --eval` / the REPL
const self = resolve(fileURLToPath(import.meta.url));
const entry = resolve(entryArg);
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
if (candidates.includes(self)) return true;
const realSelf = realOrSelf(self);
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
}

/** `realpathSync`, degrading to the input for a path that cannot be read. */
function realOrSelf(p: string): string {
try {
return realpathSync(p);
} catch {
return p;
}
}

if (isProcessEntry()) {
main().catch(error => {
console.error('❌ Error:', error);
process.exit(1);
Expand Down
41 changes: 38 additions & 3 deletions packages/core/examples/phase2-integration.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,9 @@
* in a real-world scenario.
*/

import { realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ObjectKernel,
PluginHealthMonitor,
Expand DownExpand Up@@ -350,8 +353,40 @@ async function example() {
});
}

// Run example if this file is executed directly (ES Module compatible)
// Note: In ES modules, use import.meta.url instead of require.main
if (import.meta.url === `file://${process.argv[1]}`) {
// ─── entry guard ───────────────────────────────────────────────────────
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
// the template also skips the percent-encoding `pathToFileURL` applies — so that
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
// containing a character that needs encoding (a `#` in a parent directory name is
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
//
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
// `scripts/invoked-as.mjs` (`invokedAs`). Spelled out rather than imported because
// neither home is legally reachable from this file — the PR for #10269 carries the
// boundary measurement. ⚠️ Two predicates answering this question differently IS the
// defect this closes; change one, change all of them.
function isProcessEntry(): boolean {
const entryArg = process.argv[1];
if (!entryArg) return false; // `node --eval` / the REPL
const self = resolve(fileURLToPath(import.meta.url));
const entry = resolve(entryArg);
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
if (candidates.includes(self)) return true;
const realSelf = realOrSelf(self);
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
}

/** `realpathSync`, degrading to the input for a path that cannot be read. */
function realOrSelf(p: string): string {
try {
return realpathSync(p);
} catch {
return p;
}
}

if (isProcessEntry()) {
example().catch(console.error);
}
55 changes: 53 additions & 2 deletions packages/objectql/scripts/dry-run-hash-compat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,8 @@
* the probe against synthetic fixtures covering legacy edge cases.
*/

import { realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { hashSpec } from '@objectstack/metadata-core';

export interface LegacyMetadataRow {
Expand DownExpand Up@@ -257,8 +259,57 @@ export function formatReport(report: DryRunReport): string {
return lines.join('\n');
}

// CLI entrypoint — only runs when invoked directly.
if (typeof process !== 'undefined' && process.argv[1] && /dry-run-hash-compat\.ts$/.test(process.argv[1])) {
// ─── entry guard ───────────────────────────────────────────────────────
// ⛔ NOT ``import.meta.url === `file://${process.argv[1]}` ``. Node symlink-resolves
// `import.meta.url` but leaves `process.argv[1]` exactly as the caller typed it, and
// the template also skips the percent-encoding `pathToFileURL` applies — so that
// spelling goes INERT (exit 0, no output) through a symlink AND on any checkout path
// containing a character that needs encoding (a `#` in a parent directory name is
// enough, with no symlink involved). Compare RESOLVED PATHS, never URL strings.
//
// Same predicate as `packages/cli/src/utils/invocation.ts` (`isProcessEntry`) and
// `scripts/invoked-as.mjs` (`invokedAs`) — both legs identical: realpath for the
// symlink, directory resolution for `node <dir>`. Spelled out rather than imported
// because neither home is legally reachable from this file — the PR for #10269
// carries the boundary measurement. ⚠️ Two predicates answering this question
// differently IS the defect this closes; change one, change all of them.
//
// ⚠️ ONE spelling DIVERGES from those two, and the divergence is FORCED — do not
// "restore consistency" here: the self-path seed is `__filename`, NOT
// `fileURLToPath(import.meta.url)`. `packages/objectql/package.json` declares no
// `"type"`, so under the repo-wide `module: NodeNext` every file in this package
// compiles as COMMONJS, and `import.meta` in a CommonJS-format file is a hard
// compile error (TS1470). This file IS inside a tsc program despite the package's
// own `include` naming only `src/**/*`: `src/dry-run-hash-compat.test.ts` imports
// it, and the TEST_DEBT re-measure in `scripts/check-type-check-coverage.mjs`
// type-checks the tests — so the ESM seed costs a ratchet failure on a ledger that
// may only shrink. The PREDICATE is untouched by this: `invokedAs(entryArg,
// selfPath)` is the shared core and it takes a PATH, `isEntrypoint(import.meta.url)`
// is merely the ESM way to seed it, and `__filename` is the CommonJS way — node's
// CJS loader hands it an absolute path that is ALREADY symlink-resolved and
// percent-decoded, which is exactly the property the guard rests on.
function isProcessEntry(): boolean {
const entryArg = process.argv[1];
if (!entryArg) return false; // `node --eval` / the REPL
const self = resolve(__filename);
const entry = resolve(entryArg);
// `node <dir>` gives the ENTRY ARGUMENT, and only it, directory resolution.
const candidates = [entry, join(entry, 'index.js'), join(entry, 'index.mjs'), join(entry, 'index.ts')];
if (candidates.includes(self)) return true;
const realSelf = realOrSelf(self);
return candidates.some((candidate) => realOrSelf(candidate) === realSelf);
}

/** `realpathSync`, degrading to the input for a path that cannot be read. */
function realOrSelf(p: string): string {
try {
return realpathSync(p);
} catch {
return p;
}
}

if (isProcessEntry()) {
const path = process.argv[2];
if (!path) {
console.error('Usage: pnpm tsx packages/objectql/scripts/dry-run-hash-compat.ts <snapshot.json>');
Expand Down
Loading