release: prepare WRNexusJS 0.8.8
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ai",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/benchmark",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cache",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/captcha",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
+12
-18
@@ -24,6 +24,12 @@ function literalUnion(values: Iterable<string>): string {
|
||||
return unique.length ? unique.map((value) => JSON.stringify(value)).join(" | ") : "never";
|
||||
}
|
||||
|
||||
function generatedContractMap(name: string, members: string): string {
|
||||
return members.trim()
|
||||
? `interface ${name} {\n${members}\n }`
|
||||
: `type ${name} = Record<string, never>;`;
|
||||
}
|
||||
|
||||
function typeImport(fromDirectory: string, file: string): string {
|
||||
const path = relative(fromDirectory, file).replace(/\\/g, "/");
|
||||
return JSON.stringify(path.startsWith(".") ? path : `./${path}`);
|
||||
@@ -203,24 +209,12 @@ declare namespace WRNexusGenerated {
|
||||
type TranslationKey = ${literalUnion(localeKeys)};
|
||||
type QueueName = ${literalUnion(queueNames)};
|
||||
type CacheKey = ${literalUnion(cacheKeys)};
|
||||
interface Components {
|
||||
${componentMap}
|
||||
}
|
||||
interface ApiContracts {
|
||||
${apiContracts}
|
||||
}
|
||||
interface MiddlewareContexts {
|
||||
${middlewareContracts}
|
||||
}
|
||||
interface DatabaseQueries {
|
||||
${databaseContracts}
|
||||
}
|
||||
interface RealtimeMessages {
|
||||
${realtimeContracts}
|
||||
}
|
||||
interface QueuePayloads {
|
||||
${queueContracts}
|
||||
}
|
||||
${generatedContractMap("Components", componentMap)}
|
||||
${generatedContractMap("ApiContracts", apiContracts)}
|
||||
${generatedContractMap("MiddlewareContexts", middlewareContracts)}
|
||||
${generatedContractMap("DatabaseQueries", databaseContracts)}
|
||||
${generatedContractMap("RealtimeMessages", realtimeContracts)}
|
||||
${generatedContractMap("QueuePayloads", queueContracts)}
|
||||
type ApplicationConfig = ${configFile ? `(typeof import(${typeImport(typeDir, configFile)}))["default"]` : "Record<string, never>"};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -33,6 +33,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { formatWrn, parse } from "@wrnexus/syntax";
|
||||
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
|
||||
import { inspectProject, type DoctorCheck } from "./doctor.ts";
|
||||
import { generateApplicationTypes } from "./types.ts";
|
||||
|
||||
/** The version of the CLI currently running (its own package.json). */
|
||||
function cliVersion(): string {
|
||||
@@ -1783,6 +1784,10 @@ const MIGRATIONS: Migration[] = [
|
||||
description:
|
||||
"Adds compatible imports, type checking, stores, and legacy behavior configuration.",
|
||||
apply(ctx) {
|
||||
// Workspace manifests are updated alongside each configured application,
|
||||
// but they are not themselves runnable apps. Keep app-owned declarations
|
||||
// under the individual app roots instead of creating `<workspace>/app`.
|
||||
if (!existsSync(join(ctx.appRoot, "app", "pages"))) return;
|
||||
updateV060Config(ctx);
|
||||
const types = join(ctx.appRoot, "app", "types");
|
||||
if (!existsSync(types)) {
|
||||
@@ -2115,6 +2120,17 @@ const MIGRATIONS: Migration[] = [
|
||||
// select for non-link items as well as navigation items.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.8.8",
|
||||
id: "0.8.8-editor-types-and-update-verification",
|
||||
description:
|
||||
"Adds typed page context support and regenerates lint-safe application declarations before update verification.",
|
||||
apply() {
|
||||
// No source rewrite is required. The 0.8.8 CLI regenerates application
|
||||
// declarations immediately before verification, repairing stale empty
|
||||
// interface contracts produced by earlier releases.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
@@ -2297,6 +2313,10 @@ export function blockingVerificationChecks(checks: DoctorCheck[]): DoctorCheck[]
|
||||
}
|
||||
|
||||
function verifyApp(app: UpdatedApp): boolean {
|
||||
// Generated declarations must reflect the newly installed CLI before the
|
||||
// application's lint/typecheck scripts inspect them. This also repairs stale
|
||||
// generated output left by an older framework version.
|
||||
if (existsSync(join(app.root, "app", "pages"))) generateApplicationTypes(app.root);
|
||||
const health = inspectProject(app.root);
|
||||
const failed = blockingVerificationChecks(health);
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ test("generate types emits application-wide deterministic contracts", () => {
|
||||
expect(output).toContain('"/realtime/chat": RealtimeMessage<');
|
||||
expect(output).toContain("interface QueuePayloads");
|
||||
expect(output).toContain('"email": QueuePayload<');
|
||||
expect(output).toContain("type MiddlewareContexts = Record<string, never>");
|
||||
expect(output).toContain("type DatabaseQueries = Record<string, never>");
|
||||
expect(output).not.toMatch(/interface\s+\w+\s*\{\s*\}/);
|
||||
});
|
||||
|
||||
test("application checker validates every wrn source", () => {
|
||||
|
||||
@@ -169,6 +169,30 @@ test("update verification skips unavailable scripts", () => {
|
||||
expect(verificationCommands({ check: "bun run lint" })).toEqual([["run", "check"]]);
|
||||
});
|
||||
|
||||
test("0.6 type declarations are created only inside runnable apps", () => {
|
||||
const workspaceRoot = mkdtempSync(join(tmpdir(), "wrnexus-update-workspace-"));
|
||||
const appRoot = join(workspaceRoot, "apps", "web");
|
||||
mkdirSync(join(appRoot, "app", "pages"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(workspaceRoot, "package.json"),
|
||||
JSON.stringify({ name: "workspace", wrnexus: { version: "0.5.9" } }),
|
||||
);
|
||||
writeFileSync(
|
||||
join(appRoot, "package.json"),
|
||||
JSON.stringify({ name: "web", wrnexus: { version: "0.5.9" } }),
|
||||
);
|
||||
|
||||
try {
|
||||
updateApp(workspaceRoot, "0.6.0", false);
|
||||
updateApp(appRoot, "0.6.0", false);
|
||||
|
||||
expect(existsSync(join(workspaceRoot, "app", "types", "global.d.ts"))).toBe(false);
|
||||
expect(existsSync(join(appRoot, "app", "types", "global.d.ts"))).toBe(true);
|
||||
} finally {
|
||||
rmSync(workspaceRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("0.3 WRN source normalization is conservative and idempotent", async () => {
|
||||
const { migrateWrnSource } = await import("../src/update.ts");
|
||||
const source = `component Card {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/content",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/core",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/graphql",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/helpers",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/identity",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Enterprise federation, provisioning, machine identity, and privacy governance for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/image",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/language-server",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Editor-neutral Language Server Protocol implementation for WRNexus .wrn files.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -75,7 +75,10 @@ function diagnosticRange(diagnostic: WrnDiagnostic): Range {
|
||||
};
|
||||
}
|
||||
|
||||
export function documentDiagnostics(document: TextDocument) {
|
||||
export function documentDiagnostics(
|
||||
document: TextDocument,
|
||||
options: { includeTypes?: boolean } = {},
|
||||
) {
|
||||
if (Buffer.byteLength(document.text, "utf8") > 1_048_576) {
|
||||
return [
|
||||
{
|
||||
@@ -96,7 +99,8 @@ export function documentDiagnostics(document: TextDocument) {
|
||||
message: diagnostic.message,
|
||||
}),
|
||||
);
|
||||
if (syntax.some((diagnostic) => diagnostic.severity === 1)) return syntax;
|
||||
if (syntax.some((diagnostic) => diagnostic.severity === 1) || options.includeTypes === false)
|
||||
return syntax;
|
||||
const types = checkWrnSource(document.text, { filePath: documentPath(document.uri) }).map(
|
||||
(diagnostic) => ({
|
||||
range: {
|
||||
@@ -235,4 +239,61 @@ export function hover(document: TextDocument, position: Position) {
|
||||
export function completionItems() {
|
||||
return WRN_COMPLETIONS.map((label) => ({ label, kind: 14, detail: "WRNexus language keyword" }));
|
||||
}
|
||||
|
||||
const semanticTokenTypes = ["variable"] as const;
|
||||
const semanticTokenModifiers = ["declaration", "modification"] as const;
|
||||
export const semanticTokensLegend = {
|
||||
tokenTypes: [...semanticTokenTypes],
|
||||
tokenModifiers: [...semanticTokenModifiers],
|
||||
};
|
||||
|
||||
/** Encode state declarations, reads, and writes using the LSP relative token format. */
|
||||
export function semanticTokens(document: TextDocument): { data: number[] } {
|
||||
const declarations = new Map<string, Set<number>>();
|
||||
for (const match of document.text.matchAll(
|
||||
/\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm,
|
||||
)) {
|
||||
const name = match[1]!;
|
||||
const offset = match.index! + match[0].lastIndexOf(name);
|
||||
const offsets = declarations.get(name) ?? new Set<number>();
|
||||
offsets.add(offset);
|
||||
declarations.set(name, offsets);
|
||||
}
|
||||
const comments = [...document.text.matchAll(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g)].map((match) => ({
|
||||
start: match.index!,
|
||||
end: match.index! + match[0].length,
|
||||
}));
|
||||
const tokens: Array<{ line: number; character: number; length: number; modifiers: number }> = [];
|
||||
for (const [name, offsets] of declarations) {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
for (const match of document.text.matchAll(
|
||||
new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g"),
|
||||
)) {
|
||||
const offset = match.index!;
|
||||
if (comments.some((range) => offset >= range.start && offset < range.end)) continue;
|
||||
const position = positionAt(document.text, offset);
|
||||
const after = document.text
|
||||
.slice(offset + name.length)
|
||||
.match(/^\s*(?:=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
|
||||
const before = document.text.slice(Math.max(0, offset - 8), offset).match(/(?:\+\+|--)\s*$/);
|
||||
tokens.push({
|
||||
...position,
|
||||
length: name.length,
|
||||
modifiers: offsets.has(offset) ? 1 : after || before ? 2 : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
tokens.sort((a, b) => a.line - b.line || a.character - b.character);
|
||||
const data: number[] = [];
|
||||
let previousLine = 0;
|
||||
let previousCharacter = 0;
|
||||
for (const token of tokens) {
|
||||
const deltaLine = token.line - previousLine;
|
||||
const deltaCharacter = deltaLine === 0 ? token.character - previousCharacter : token.character;
|
||||
data.push(deltaLine, deltaCharacter, token.length, 0, token.modifiers);
|
||||
previousLine = token.line;
|
||||
previousCharacter = token.character;
|
||||
}
|
||||
return { data };
|
||||
}
|
||||
export * from "./workspace.ts";
|
||||
|
||||
@@ -6,10 +6,15 @@ import {
|
||||
documentSymbols,
|
||||
formatDocument,
|
||||
hover,
|
||||
symbolLocations,
|
||||
offsetAt,
|
||||
positionAt,
|
||||
semanticTokens,
|
||||
semanticTokensLegend,
|
||||
wordAt,
|
||||
virtualTypeScriptDocument,
|
||||
workspaceCompletionItems,
|
||||
workspaceSymbolLocations,
|
||||
clearWorkspaceIndexCache,
|
||||
extractComponentRefactor,
|
||||
htmlToWrn,
|
||||
type TextDocument,
|
||||
@@ -47,9 +52,9 @@ function internalDiagnostic(document: TextDocument, error: unknown) {
|
||||
};
|
||||
}
|
||||
|
||||
function safeDocumentDiagnostics(document: TextDocument) {
|
||||
function safeDocumentDiagnostics(document: TextDocument, includeTypes = true) {
|
||||
try {
|
||||
return documentDiagnostics(document);
|
||||
return documentDiagnostics(document, { includeTypes });
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
||||
@@ -58,11 +63,11 @@ function safeDocumentDiagnostics(document: TextDocument) {
|
||||
}
|
||||
}
|
||||
|
||||
function publish(document: TextDocument): void {
|
||||
function publish(document: TextDocument, includeTypes = true): void {
|
||||
send({
|
||||
jsonrpc: "2.0",
|
||||
method: "textDocument/publishDiagnostics",
|
||||
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document) },
|
||||
params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document, includeTypes) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,7 +85,7 @@ function schedulePublish(document: TextDocument): void {
|
||||
setTimeout(() => {
|
||||
diagnosticTimers.delete(document.uri);
|
||||
const current = documents.get(document.uri);
|
||||
if (current && current.version === expectedVersion) publish(current);
|
||||
if (current && current.version === expectedVersion) publish(current, false);
|
||||
}, DIAGNOSTIC_DEBOUNCE_MS),
|
||||
);
|
||||
}
|
||||
@@ -104,7 +109,7 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
result(message.id, {
|
||||
serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
|
||||
capabilities: {
|
||||
textDocumentSync: 1,
|
||||
textDocumentSync: { openClose: true, change: 1, save: true },
|
||||
documentFormattingProvider: true,
|
||||
completionProvider: { triggerCharacters: ["<", "@", ":", "."] },
|
||||
hoverProvider: true,
|
||||
@@ -112,8 +117,14 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
referencesProvider: true,
|
||||
renameProvider: { prepareProvider: true },
|
||||
documentSymbolProvider: true,
|
||||
semanticTokensProvider: { legend: semanticTokensLegend, full: true },
|
||||
codeActionProvider: {
|
||||
codeActionKinds: ["quickfix", "refactor.extract", "refactor.rewrite"],
|
||||
codeActionKinds: [
|
||||
"quickfix",
|
||||
"refactor.extract",
|
||||
"refactor.rewrite",
|
||||
"source.organizeImports",
|
||||
],
|
||||
},
|
||||
experimental: { wrnexusVirtualTypeScript: true },
|
||||
},
|
||||
@@ -127,6 +138,9 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
result(message.id, null);
|
||||
break;
|
||||
case "exit":
|
||||
for (const timer of diagnosticTimers.values()) clearTimeout(timer);
|
||||
diagnosticTimers.clear();
|
||||
documents.clear();
|
||||
process.exit(0);
|
||||
break;
|
||||
case "textDocument/didOpen": {
|
||||
@@ -146,6 +160,15 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "textDocument/didSave": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
clearDiagnosticTimer(document.uri);
|
||||
clearWorkspaceIndexCache(workspaceRoot);
|
||||
publish(document, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "textDocument/didClose":
|
||||
clearDiagnosticTimer(params.textDocument.uri);
|
||||
documents.delete(params.textDocument.uri);
|
||||
@@ -180,6 +203,11 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
result(message.id, document ? documentSymbols(document) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/semanticTokens/full": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? semanticTokens(document) : { data: [] });
|
||||
break;
|
||||
}
|
||||
case "textDocument/hover": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? hover(document, params.position) : null);
|
||||
@@ -192,7 +220,8 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
}
|
||||
case "textDocument/references": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
result(message.id, document ? symbolLocations(document, params.position) : []);
|
||||
const selected = document ? wordAt(document.text, params.position) : null;
|
||||
result(message.id, selected ? workspaceSymbolLocations(workspaceRoot, selected.word) : []);
|
||||
break;
|
||||
}
|
||||
case "textDocument/prepareRename": {
|
||||
@@ -202,13 +231,16 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
}
|
||||
case "textDocument/rename": {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const edits = document
|
||||
? symbolLocations(document, params.position).map(({ range }) => ({
|
||||
range,
|
||||
newText: params.newName,
|
||||
}))
|
||||
: [];
|
||||
result(message.id, document ? { changes: { [document.uri]: edits } } : null);
|
||||
const selected = document ? wordAt(document.text, params.position) : null;
|
||||
if (!selected || !/^[A-Za-z_$][\w$]*$/.test(params.newName)) {
|
||||
result(message.id, null);
|
||||
break;
|
||||
}
|
||||
const changes: Record<string, Array<{ range: any; newText: string }>> = {};
|
||||
for (const location of workspaceSymbolLocations(workspaceRoot, selected.word)) {
|
||||
(changes[location.uri] ??= []).push({ range: location.range, newText: params.newName });
|
||||
}
|
||||
result(message.id, { changes });
|
||||
break;
|
||||
}
|
||||
case "textDocument/codeAction": {
|
||||
@@ -231,6 +263,71 @@ async function handle(message: JsonRpc): Promise<void> {
|
||||
},
|
||||
},
|
||||
}));
|
||||
const imports = [
|
||||
...document.text.matchAll(
|
||||
/^import(?:\s+type)?[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm,
|
||||
),
|
||||
].map((match) => ({
|
||||
start: match.index!,
|
||||
end: match.index! + match[0].length,
|
||||
text: match[0].trim(),
|
||||
}));
|
||||
if (imports.length > 1) {
|
||||
const ordered = [...imports].sort((a, b) => {
|
||||
const aType = /^import\s+type\b/.test(a.text) ? 0 : 1;
|
||||
const bType = /^import\s+type\b/.test(b.text) ? 0 : 1;
|
||||
return aType - bType || a.text.localeCompare(b.text);
|
||||
});
|
||||
const replacement = ordered.map((entry) => entry.text.replace(/;$/, "")).join("\n") + "\n";
|
||||
const start = imports[0]!.start;
|
||||
const end = imports.at(-1)!.end;
|
||||
if (document.text.slice(start, end) !== replacement) {
|
||||
actions.push({
|
||||
title: "Organize WRN imports",
|
||||
kind: "source.organizeImports",
|
||||
edit: {
|
||||
changes: {
|
||||
[document.uri]: [
|
||||
{
|
||||
range: {
|
||||
start: positionAt(document.text, start),
|
||||
end: positionAt(document.text, end),
|
||||
},
|
||||
newText: replacement,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const diagnostic of params.context?.diagnostics ?? []) {
|
||||
if (diagnostic.code !== "WRN-OUTPUT-LEGACY-EMIT") continue;
|
||||
const start = offsetAt(document.text, diagnostic.range.start);
|
||||
const end = offsetAt(document.text, diagnostic.range.end);
|
||||
const source = document.text.slice(start, end);
|
||||
const match = /\$emit\(\s*["']([A-Za-z_$][\w$]*)["']\s*,?/.exec(source);
|
||||
if (!match) continue;
|
||||
actions.push({
|
||||
title: `Convert $emit to output.${match[1]}`,
|
||||
kind: "quickfix",
|
||||
diagnostics: [diagnostic],
|
||||
isPreferred: true,
|
||||
edit: {
|
||||
changes: {
|
||||
[document.uri]: [
|
||||
{
|
||||
range: diagnostic.range,
|
||||
newText: source.replace(
|
||||
/\$emit\(\s*["'][A-Za-z_$][\w$]*["']\s*,?\s*/,
|
||||
`output.${match[1]}(`,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const selected = document.text.slice(
|
||||
document.text
|
||||
.split(/\r?\n/)
|
||||
|
||||
@@ -15,7 +15,9 @@ const SKIPPED_DIRECTORIES = new Set([
|
||||
]);
|
||||
const MAX_INDEX_FILES = 5_000;
|
||||
const MAX_SOURCE_BYTES = 1_048_576;
|
||||
const INDEX_TTL_MS = 5_000;
|
||||
// Completion can be requested repeatedly while the list is visible. Avoid
|
||||
// rescanning thousands of files on that hot path; document saves invalidate it.
|
||||
const INDEX_TTL_MS = 5 * 60_000;
|
||||
const MAX_CACHED_ROOTS = 8;
|
||||
const workspaceIndexCache = new Map<
|
||||
string,
|
||||
@@ -168,6 +170,30 @@ export function clearWorkspaceIndexCache(root?: string): void {
|
||||
else workspaceIndexCache.clear();
|
||||
}
|
||||
|
||||
export function workspaceSymbolLocations(root: string, name: string, limit = 1_000) {
|
||||
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return [];
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g");
|
||||
const locations: Array<{ uri: string; range: Range }> = [];
|
||||
for (const file of walk(resolve(root), (path) => extname(path) === ".wrn").slice(0, limit)) {
|
||||
let source: string;
|
||||
try {
|
||||
source = readFileSync(file, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
const before = source.slice(0, match.index!).split(/\r?\n/);
|
||||
const start = { line: before.length - 1, character: before.at(-1)?.length ?? 0 };
|
||||
locations.push({
|
||||
uri: `file:///${file.replace(/\\/g, "/")}`,
|
||||
range: { start, end: { line: start.line, character: start.character + name.length } },
|
||||
});
|
||||
}
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
export function extractComponentRefactor(document: TextDocument, range: Range, name: string) {
|
||||
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name)) throw new Error("Component name must use PascalCase");
|
||||
const lines = document.text.split(/\r?\n/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mcp",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Model Context Protocol server exposing WRNexus application and framework context.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mobile",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/native",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/oauth",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/observability",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/playground",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Secure, shareable WRNexus compiler and UI playground.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/plugin",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pubsub",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pwa",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/reactive",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/realtime",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"description": "Typed realtime rooms, message helpers, presence utilities, package UI blocks, and WRNexusJS integration.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/rpc",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/security",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ssr",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/store",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/tracking",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -239,6 +239,10 @@ export function virtualTypeScriptModule(
|
||||
|
||||
append(retainedImports(ast), ast.imports[0]);
|
||||
append(ast.types.join("\n\n"), ast.types[0]?.trim());
|
||||
// Pages, components, and stores are rendered with the framework request
|
||||
// context in scope. Mirror the compiler-generated function parameter here so
|
||||
// expressions such as `state user = ctx.user` type-check in the editor.
|
||||
append('declare const ctx: import("@wrnexus/core").Context;');
|
||||
|
||||
for (const prop of ast.props) {
|
||||
// JavaScript keywords such as the conventional `class` component prop are
|
||||
|
||||
@@ -73,3 +73,15 @@ test("treats omitted runtime parameter types as dynamic JavaScript values", () =
|
||||
);
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("provides the framework context to page state expressions", () => {
|
||||
const root = app();
|
||||
const diagnostics = checkWrnSource(
|
||||
`page AccountPage {
|
||||
state user = ctx.user
|
||||
view { <main></main> }
|
||||
}`,
|
||||
{ appRoot: root, filePath: join(root, "app", "pages", "account.wrn") },
|
||||
);
|
||||
expect(diagnostics.some((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user