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
33 changes: 21 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ jobs:
git config --global core.autocrlf false
git config --global core.eol lf

- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Cache OCaml's opam
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.opam
key: ${{matrix.os}}-rescript-vscode-v4
key: ${{ runner.os }}-${{ runner.arch }}-ocaml-5.3.0-${{ hashFiles('dune-project', 'resgraph.opam') }}

- name: Use OCaml
uses: ocaml/setup-ocaml@v3
Expand Down Expand Up @@ -81,6 +81,13 @@ jobs:
if: matrix.os == 'ubuntu-22.04'
run: opam exec -- make checkformat

- name: Typecheck and bundle VS Code extension
if: matrix.os == 'ubuntu-22.04'
working-directory: vscode-extension
run: |
npm ci
npm run build

- name: Verify generated outputs are clean
run: |
status=$(git status --porcelain)
Expand Down Expand Up @@ -112,7 +119,9 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Use Node.js
uses: actions/setup-node@v4
Expand Down Expand Up @@ -156,16 +165,16 @@ jobs:
run: rm binary.tar
working-directory: ./bin

- name: Store short commit SHA for filename
id: vars
env:
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: echo "::set-output name=sha_short::${COMMIT_SHA:0:7}"
- name: Test packed package as a consumer
run: npm run test:package

- name: Store tag name
id: tag_name
- name: Verify release metadata and main ancestry
if: startsWith(github.ref, 'refs/tags/')
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
run: |
node scripts/verify-release.mjs "$GITHUB_REF_NAME"
git fetch origin main
git merge-base --is-ancestor HEAD origin/main


- name: Package release
run: npm pack
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
tests/lib
tests/**/*.mjs
!tests/runtime-interface-returns.mjs
!tests/runtime-bindings.mjs
tests/node_modules
tests/.bsb.lock
_build
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
# ResGraph Changelog

## Unreleased
- Validate operations before execution, support synchronous GraphQL.js results,
return query syntax, validation, and malformed-variable failures through the
execution-result envelope, and avoid redundant query hashing.
- Add correct keyed DataLoader priming and type-safe per-entry batch results,
deprecate the unsound legacy priming and `loadMany` signatures, and preserve
array order in structural cache keys. Non-JavaScript ReScript exceptions are
normalized for DataLoader and retain their identity in `loadManyResults`.
- Preserve last-known-good generated schemas on validation failures and use
atomic writes for generated files and native state.
- Mark legacy schema modules as generated, preserve unmarked user-owned files
during named-schema migration, and surface ownership persistence failures.
- Add a packed-package consumer test, native architecture fixtures, strict
integration failure propagation, release guards, and VS Code extension checks.
- Extract the native generator into a wrapped engine library with a reusable
generation context; batch schemas by compiler root to share CMT summaries
while retaining per-schema results; and publish architecture and
configuration-schema docs.
- Scope shared native summaries by package and keep artifact-write failures
inside a single structured batch response.
- Preserve numeric and string JSON-RPC IDs and contain language-server handler
failures. Completion buffers now use private, OS-created temporary directories
that are cleaned after both successful and failed requests.

## 1.3.0

Expand Down
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
SHELL = /bin/bash

build-resgraph-binary:
rm -f bin/dev/resgraph.exe
dune build --profile release
cp _build/install/default/bin/resgraph bin/dev/resgraph.exe

build-resgraph-binary-dev:
rm -f bin/dev/resgraph.exe
dune build
cp _build/install/default/bin/resgraph bin/dev/resgraph.exe

build-cli:
npm run build

build-tests:
make -C tests build

build: build-resgraph-binary build-tests
build: build-resgraph-binary build-cli build-tests

dce: build-resgraph-binary
opam exec reanalyze.exe -- -dce-cmt _build -suppress vendor

format:
dune build @fmt --auto-promote

test-resgraph-binary: build-resgraph-binary
test-resgraph-binary: build-resgraph-binary build-cli
make -C tests test

test: test-resgraph-binary
Expand All @@ -25,11 +33,10 @@ clean:
rm -f bin/dev/resgraph.exe
dune clean
make -C tests clean
make -C reanalyze clean

checkformat:
dune build @fmt

.DEFAULT_GOAL := build

.PHONY: build-resgraph-binary build-tests dce clean format test
.PHONY: build-resgraph-binary build-resgraph-binary-dev build-cli build-tests dce clean format test
55 changes: 40 additions & 15 deletions cli/Cli.res
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let printAuthorizationBaselineWarning = (authorization: option<Utils.authorizati
}

let helpText = `
**ResGraph v0.1.0 CLI**
**ResGraph CLI**
This is the CLI of ResGraph. All configuration is read from \`resgraph.json\`.
Available commands:

Expand Down Expand Up @@ -150,29 +150,52 @@ let printFindDefinition = (~target, ~jsonOutput, ~schemaName) => {
let buildSchemas = (config: Utils.config, schemas: array<Utils.schemaConfig>) => {
let showSchemaName = !config.legacy || schemas->Array.length > 1
let hadError = ref(false)
let schemasByCompilerRoot: Dict.t<array<Utils.schemaConfig>> = Dict.make()

schemas->Array.forEach(schema => {
let compilerRoot =
schema.projectRoot->Utils.findCompilerRoot->Option.getOr(schema.projectRoot)->Utils.canonicalPath
switch schemasByCompilerRoot->Dict.get(compilerRoot) {
| Some(group) => group->Array.push(schema)
| None => schemasByCompilerRoot->Dict.set(compilerRoot, [schema])
}
})

schemasByCompilerRoot->Dict.toArray->Array.forEach(((_compilerRoot, group)) => {
let timeStart = performance->now
try {
switch Utils.callPrivateCli(GenerateSchema(schema)) {
| Completion(_) | Hover(_) | Definition(_) | FindDefinition(_) | NotInitialized => ()
| Success(_) =>
printBuildTime(schema, performance->now -. timeStart, ~showSchemaName)
printAuthorizationBaselineWarning(schema.authorization)
| Error({errors}) =>
if showSchemaName {
Console.error(`[${schema.name}] Schema generation failed.`)
}
ErrorPrinter.printErrors(errors)
hadError := true
let results = Utils.callPrivateCliBatch(group)
if results->Array.length !== group->Array.length {
panic("Native batch response did not match the requested schema count.")
}
results->Array.forEachWithIndex((result, index) => {
let schema = group[index]->Option.getOrThrow(~message="Missing schema for batch result.")
switch result {
| Completion(_) | Hover(_) | Definition(_) | FindDefinition(_) | NotInitialized =>
Console.error(`[${schema.name}] Native generator returned an unexpected response.`)
hadError := true
| Success(_) =>
printBuildTime(schema, performance->now -. timeStart, ~showSchemaName)
printAuthorizationBaselineWarning(schema.authorization)
| Error({errors}) =>
if showSchemaName {
Console.error(`[${schema.name}] Schema generation failed.`)
}
ErrorPrinter.printErrors(errors)
hadError := true
}
})
} catch {
| Exn.Error(error) =>
Console.error(`[${schema.name}] Generator process failed.`)
group->Array.forEach(schema =>
Console.error(`[${schema.name}] Generator process failed.`)
)
Console.error(error)
hadError := true
| _ =>
Console.error(`[${schema.name}] Generator process failed.`)
group->Array.forEach(schema =>
Console.error(`[${schema.name}] Generator process failed.`)
)
hadError := true
}
})
Expand Down Expand Up @@ -316,9 +339,11 @@ try {
Process.process->Process.exitWithCode(1)
}
| list{"help"} => Console.log(helpText)
| list{"tools"} => Console.log(toolsHelpText)
| value =>
Console.log("Invalid command: " ++ value->List.toArray->Array.join(" "))
Console.error("Invalid command: " ++ value->List.toArray->Array.join(" "))
Console.log(helpText)
Process.process->Process.exitWithCode(1)
}
} catch {
| Exn.Error(error) =>
Expand Down
10 changes: 5 additions & 5 deletions cli/ErrorPrinter.res
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ let prettyPrintDiagnostic = (~lines, ~diagnostic: Utils.generateError) => {

`${colors.red("Error in file:")} ${colors.blueBright(
diagnostic.file,
)}:${fileLocText}`->Console.log
Console.log("\n")
)}:${fileLocText}`->Console.error
Console.error("\n")
lines->Array.forEachWithIndex((line, index) => {
if index > diagnostic.range.start.line - 5 && index < diagnostic.range.end_.line + 5 {
let highlightOnThisLine =
Expand All @@ -60,18 +60,18 @@ let prettyPrintDiagnostic = (~lines, ~diagnostic: Utils.generateError) => {
) ++
line->String.slice(~start=highlightEndOffset)

Console.log(
Console.error(
` ${modifiers.bold.red(
Int.toString(index + 1),
)} ${colors.blackBright(`┆`)} ${lineText}`,
)
} else {
Console.log(` ${Int.toString(index + 1)} ${colors.blackBright(`┆`)} ${line}`)
Console.error(` ${Int.toString(index + 1)} ${colors.blackBright(`┆`)} ${line}`)
}
}
})

Console.log("\n " ++ diagnostic.message)
Console.error("\n " ++ diagnostic.message)
}

let printErrors = (errors: array<Utils.generateError>) => {
Expand Down
49 changes: 27 additions & 22 deletions cli/GeneratedArtifacts.res
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ let fileStartsWith = (path, prefix) =>

let removeFile = path => {
if Fs.existsSync(path) {
try {
Fs.unlinkSync(path)
} catch {
| _ => ()
}
Fs.unlinkSync(path)
}
}

let removeFileIgnoringErrors = path => {
try {
path->removeFile
} catch {
| _ => ()
}
}

Expand All @@ -103,22 +107,18 @@ let cleanOwnership = ownership => {

if Fs.existsSync(ownership.outputFolder) {
let interfacePrefix = ownership.moduleName ++ "__Interface_"
try {
ownership.outputFolder
->Fs.readdirSync
->Array.forEach(fileName => {
let path = Path.resolve([ownership.outputFolder, fileName])
if (
fileName->String.startsWith(interfacePrefix) &&
fileName->String.endsWith(".res") &&
path->fileStartsWith(generatedInterfaceHeader)
) {
path->removeFile
}
})
} catch {
| _ => ()
}
ownership.outputFolder
->Fs.readdirSync
->Array.forEach(fileName => {
let path = Path.resolve([ownership.outputFolder, fileName])
if (
fileName->String.startsWith(interfacePrefix) &&
fileName->String.endsWith(".res") &&
path->fileStartsWith(generatedInterfaceHeader)
) {
path->removeFile
}
})
}

let schemaSdl = Path.resolve([ownership.outputFolder, "schema.graphql"])
Expand All @@ -144,7 +144,12 @@ let writeManifest = (path, schemas) => {
Fs.writeFileSync(temporaryPath, Buffer.fromString(payload->JSON.stringifyAny->Option.getOr("")))
Fs.renameSync(~from=temporaryPath, ~to_=path)
} catch {
| _ => temporaryPath->removeFile
| Exn.Error(error) =>
temporaryPath->removeFileIgnoringErrors
Utils.rethrow(error)
| _ =>
temporaryPath->removeFileIgnoringErrors
panic("Unknown failure while writing the ResGraph schema ownership manifest.")
}
}

Expand Down
Loading
Loading