feat: complete SSR CRM and refine auth UI
Quality / quality (ubuntu-latest) (push) Failing after 11m17s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-20 16:17:59 +05:30
parent f57bd05a03
commit cd0dffa87d
49 changed files with 1640 additions and 139 deletions
+25 -4
View File
@@ -93,6 +93,25 @@
"typescript": "^6.0.3", "typescript": "^6.0.3",
}, },
}, },
"examples/crm-app": {
"name": "wrnexus-crm-example",
"version": "0.8.0",
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/styles": "workspace:*",
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.123",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
},
},
"examples/i18n-showcase": { "examples/i18n-showcase": {
"name": "i18n-showcase", "name": "i18n-showcase",
"version": "0.8.0", "version": "0.8.0",
@@ -216,7 +235,7 @@
}, },
"packages/auth": { "packages/auth": {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.12", "version": "0.8.13",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*", "@wrnexus/captcha": "workspace:*",
@@ -272,7 +291,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.46", "version": "0.8.47",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -337,7 +356,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.41", "version": "0.8.42",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
@@ -642,7 +661,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.20", "version": "0.8.21",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
@@ -1406,6 +1425,8 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+6 -2
View File
@@ -1840,7 +1840,7 @@
"Position", "Position",
"Range", "Range",
"TextDocument", "TextDocument",
"WRN_COMPLETIONS", "WRN_KEYWORDS",
"WorkspaceCompletionItem", "WorkspaceCompletionItem",
"clearWorkspaceIndexCache", "clearWorkspaceIndexCache",
"completionItems", "completionItems",
@@ -1861,7 +1861,11 @@
"workspaceCompletionItems", "workspaceCompletionItems",
"workspaceSymbolLocations" "workspaceSymbolLocations"
], ],
"./server": [] "./server": [
"ApiCallCompletionItem",
"apiCallCompletions",
"apiCallHover"
]
}, },
"@wrnexus/mcp": { "@wrnexus/mcp": {
".": [ ".": [
+182 -93
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
// WRN editor language server source hash: f75d793688bdb42225d2314660c87852a108b76311974be9307d11e42ecfbb41 // WRN editor language server source hash: 70d2454817cc3aa546304c880e0a82313bb73b50051556bf7febd12e6a46c78c
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72 // WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs // @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create; (function(exports, require, module, __filename, __dirname) {var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf; var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty; var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) { function __accessProp(key) {
return this[key]; return this[key];
@@ -32,7 +33,37 @@ var __toESM = (mod, isNodeMode, target) => {
cache.set(mod, to); cache.set(mod, to);
return to; return to;
}; };
var __toCommonJS = (from) => {
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
if (entry)
return entry;
entry = __defProp({}, "__esModule", { value: true });
if (from && typeof from === "object" || typeof from === "function") {
for (var key of __getOwnPropNames(from))
if (!__hasOwnProp.call(entry, key))
__defProp(entry, key, {
get: __accessProp.bind(from, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
__moduleCache.set(from, entry);
return entry;
};
var __moduleCache;
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
});
};
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js // node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
var require_typescript = __commonJS((exports2, module2) => { var require_typescript = __commonJS((exports2, module2) => {
@@ -54,10 +85,10 @@ var require_typescript = __commonJS((exports2, module2) => {
var ts = {}; var ts = {};
((module3) => { ((module3) => {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
var __hasOwnProp2 = Object.prototype.hasOwnProperty; var __hasOwnProp2 = Object.prototype.hasOwnProperty;
var __export = (target, all) => { var __export2 = (target, all) => {
for (var name in all) for (var name in all)
__defProp2(target, name, { get: all[name], enumerable: true }); __defProp2(target, name, { get: all[name], enumerable: true });
}; };
@@ -65,13 +96,13 @@ var require_typescript = __commonJS((exports2, module2) => {
if (from && typeof from === "object" || typeof from === "function") { if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames2(from)) for (let key of __getOwnPropNames2(from))
if (!__hasOwnProp2.call(to, key) && key !== except) if (!__hasOwnProp2.call(to, key) && key !== except)
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
} }
return to; return to;
}; };
var __toCommonJS = (mod) => (__copyProps, mod); var __toCommonJS2 = (mod) => (__copyProps, mod);
var typescript_exports = {}; var typescript_exports = {};
__export(typescript_exports, { __export2(typescript_exports, {
ANONYMOUS: () => ANONYMOUS, ANONYMOUS: () => ANONYMOUS,
AccessFlags: () => AccessFlags, AccessFlags: () => AccessFlags,
AssertionLevel: () => AssertionLevel, AssertionLevel: () => AssertionLevel,
@@ -2321,7 +2352,7 @@ var require_typescript = __commonJS((exports2, module2) => {
writeFileEnsuringDirectories: () => writeFileEnsuringDirectories, writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
zipWith: () => zipWith zipWith: () => zipWith
}); });
module3.exports = __toCommonJS(typescript_exports); module3.exports = __toCommonJS2(typescript_exports);
var versionMajorMinor = "6.0"; var versionMajorMinor = "6.0";
var version = "6.0.3"; var version = "6.0.3";
var Comparison = /* @__PURE__ */ ((Comparison3) => { var Comparison = /* @__PURE__ */ ((Comparison3) => {
@@ -4851,8 +4882,8 @@ ${lanes.join(`
return compareValues(left.length, right.length); return compareValues(left.length, right.length);
} }
var VersionRange = class _VersionRange { var VersionRange = class _VersionRange {
constructor(spec2) { constructor(spec3) {
this._alternatives = spec2 ? Debug.checkDefined(parseRange(spec2), "Invalid range spec.") : emptyArray; this._alternatives = spec3 ? Debug.checkDefined(parseRange(spec3), "Invalid range spec.") : emptyArray;
} }
static tryParse(text) { static tryParse(text) {
const sets = parseRange(text); const sets = parseRange(text);
@@ -5055,7 +5086,7 @@ ${lanes.join(`
} }
var timestamp = nativePerformanceTime ? () => nativePerformanceTime.now() : Date.now; var timestamp = nativePerformanceTime ? () => nativePerformanceTime.now() : Date.now;
var ts_performance_exports = {}; var ts_performance_exports = {};
__export(ts_performance_exports, { __export2(ts_performance_exports, {
clearMarks: () => clearMarks, clearMarks: () => clearMarks,
clearMeasures: () => clearMeasures, clearMeasures: () => clearMeasures,
createTimer: () => createTimer, createTimer: () => createTimer,
@@ -21581,19 +21612,19 @@ ${lanes.join(`
if (specs === undefined || specs.length === 0) { if (specs === undefined || specs.length === 0) {
return; return;
} }
return flatMap(specs, (spec2) => spec2 && getSubPatternFromSpec(spec2, basePath, usage, wildcardMatchers[usage])); return flatMap(specs, (spec3) => spec3 && getSubPatternFromSpec(spec3, basePath, usage, wildcardMatchers[usage]));
} }
function isImplicitGlob(lastPathComponent) { function isImplicitGlob(lastPathComponent) {
return !/[.*?]/.test(lastPathComponent); return !/[.*?]/.test(lastPathComponent);
} }
function getPatternFromSpec(spec2, basePath, usage) { function getPatternFromSpec(spec3, basePath, usage) {
const pattern = spec2 && getSubPatternFromSpec(spec2, basePath, usage, wildcardMatchers[usage]); const pattern = spec3 && getSubPatternFromSpec(spec3, basePath, usage, wildcardMatchers[usage]);
return pattern && `^(?:${pattern})${usage === "exclude" ? "(?:$|/)" : "$"}`; return pattern && `^(?:${pattern})${usage === "exclude" ? "(?:$|/)" : "$"}`;
} }
function getSubPatternFromSpec(spec2, basePath, usage, { singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter: replaceWildcardCharacter2 } = wildcardMatchers[usage]) { function getSubPatternFromSpec(spec3, basePath, usage, { singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter: replaceWildcardCharacter2 } = wildcardMatchers[usage]) {
let subpattern = ""; let subpattern = "";
let hasWrittenComponent = false; let hasWrittenComponent = false;
const components = getNormalizedPathComponents(spec2, basePath); const components = getNormalizedPathComponents(spec3, basePath);
const lastComponent = last(components); const lastComponent = last(components);
if (usage !== "exclude" && lastComponent === "**") { if (usage !== "exclude" && lastComponent === "**") {
return; return;
@@ -40722,8 +40753,8 @@ ${lanes.join(`
const wildcardFiles = arrayFrom(wildcardFileMap.values()); const wildcardFiles = arrayFrom(wildcardFileMap.values());
return literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values())); return literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values()));
} }
function isExcludedFile(pathToCheck, spec2, basePath, useCaseSensitiveFileNames2, currentDirectory) { function isExcludedFile(pathToCheck, spec3, basePath, useCaseSensitiveFileNames2, currentDirectory) {
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec2; const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec3;
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs)) if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs))
return false; return false;
basePath = normalizePath(basePath); basePath = normalizePath(basePath);
@@ -40745,7 +40776,7 @@ ${lanes.join(`
return lastDotIndex > wildcardIndex; return lastDotIndex > wildcardIndex;
} }
function matchesExclude(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory) { function matchesExclude(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory) {
return matchesExcludeWorker(pathToCheck, filter(excludeSpecs, (spec2) => !invalidDotDotAfterRecursiveWildcard(spec2)), useCaseSensitiveFileNames2, currentDirectory); return matchesExcludeWorker(pathToCheck, filter(excludeSpecs, (spec3) => !invalidDotDotAfterRecursiveWildcard(spec3)), useCaseSensitiveFileNames2, currentDirectory);
} }
function matchesExcludeWorker(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory, basePath) { function matchesExcludeWorker(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory, basePath) {
const excludePattern = getRegularExpressionForWildcard(excludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude"); const excludePattern = getRegularExpressionForWildcard(excludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude");
@@ -40757,26 +40788,26 @@ ${lanes.join(`
return !hasExtension(pathToCheck) && excludeRegex.test(ensureTrailingDirectorySeparator(pathToCheck)); return !hasExtension(pathToCheck) && excludeRegex.test(ensureTrailingDirectorySeparator(pathToCheck));
} }
function validateSpecs(specs, errors, disallowTrailingRecursion, jsonSourceFile, specKey) { function validateSpecs(specs, errors, disallowTrailingRecursion, jsonSourceFile, specKey) {
return specs.filter((spec2) => { return specs.filter((spec3) => {
if (!isString(spec2)) if (!isString(spec3))
return false; return false;
const diag2 = specToDiagnostic(spec2, disallowTrailingRecursion); const diag2 = specToDiagnostic(spec3, disallowTrailingRecursion);
if (diag2 !== undefined) { if (diag2 !== undefined) {
errors.push(createDiagnostic(...diag2)); errors.push(createDiagnostic(...diag2));
} }
return diag2 === undefined; return diag2 === undefined;
}); });
function createDiagnostic(message, spec2) { function createDiagnostic(message, spec3) {
const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec2); const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec3);
return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element, message, spec2); return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element, message, spec3);
} }
} }
function specToDiagnostic(spec2, disallowTrailingRecursion) { function specToDiagnostic(spec3, disallowTrailingRecursion) {
Debug.assert(typeof spec2 === "string"); Debug.assert(typeof spec3 === "string");
if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec2)) { if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec3)) {
return [Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec2]; return [Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec3];
} else if (invalidDotDotAfterRecursiveWildcard(spec2)) { } else if (invalidDotDotAfterRecursiveWildcard(spec3)) {
return [Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec2]; return [Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec3];
} }
} }
function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }, basePath, useCaseSensitiveFileNames2) { function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }, basePath, useCaseSensitiveFileNames2) {
@@ -40787,11 +40818,11 @@ ${lanes.join(`
if (include !== undefined) { if (include !== undefined) {
const recursiveKeys = []; const recursiveKeys = [];
for (const file of include) { for (const file of include) {
const spec2 = normalizePath(combinePaths(basePath, file)); const spec3 = normalizePath(combinePaths(basePath, file));
if (excludeRegex && excludeRegex.test(spec2)) { if (excludeRegex && excludeRegex.test(spec3)) {
continue; continue;
} }
const match = getWildcardDirectoryFromSpec(spec2, useCaseSensitiveFileNames2); const match = getWildcardDirectoryFromSpec(spec3, useCaseSensitiveFileNames2);
if (match) { if (match) {
const { key, path, flags } = match; const { key, path, flags } = match;
const existingPath = wildCardKeyToPath.get(key); const existingPath = wildCardKeyToPath.get(key);
@@ -40822,20 +40853,20 @@ ${lanes.join(`
function toCanonicalKey(path, useCaseSensitiveFileNames2) { function toCanonicalKey(path, useCaseSensitiveFileNames2) {
return useCaseSensitiveFileNames2 ? path : toFileNameLowerCase(path); return useCaseSensitiveFileNames2 ? path : toFileNameLowerCase(path);
} }
function getWildcardDirectoryFromSpec(spec2, useCaseSensitiveFileNames2) { function getWildcardDirectoryFromSpec(spec3, useCaseSensitiveFileNames2) {
const match = wildcardDirectoryPattern.exec(spec2); const match = wildcardDirectoryPattern.exec(spec3);
if (match) { if (match) {
const questionWildcardIndex = spec2.indexOf("?"); const questionWildcardIndex = spec3.indexOf("?");
const starWildcardIndex = spec2.indexOf("*"); const starWildcardIndex = spec3.indexOf("*");
const lastDirectorySeperatorIndex = spec2.lastIndexOf(directorySeparator); const lastDirectorySeperatorIndex = spec3.lastIndexOf(directorySeparator);
return { return {
key: toCanonicalKey(match[0], useCaseSensitiveFileNames2), key: toCanonicalKey(match[0], useCaseSensitiveFileNames2),
path: match[0], path: match[0],
flags: questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex || starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex ? 1 : 0 flags: questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex || starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex ? 1 : 0
}; };
} }
if (isImplicitGlob(spec2.substring(spec2.lastIndexOf(directorySeparator) + 1))) { if (isImplicitGlob(spec3.substring(spec3.lastIndexOf(directorySeparator) + 1))) {
const path = removeTrailingDirectorySeparator(spec2); const path = removeTrailingDirectorySeparator(spec3);
return { return {
key: toCanonicalKey(path, useCaseSensitiveFileNames2), key: toCanonicalKey(path, useCaseSensitiveFileNames2),
path, path,
@@ -45962,7 +45993,7 @@ ${lanes.join(`
} }
} }
var ts_moduleSpecifiers_exports = {}; var ts_moduleSpecifiers_exports = {};
__export(ts_moduleSpecifiers_exports, { __export2(ts_moduleSpecifiers_exports, {
RelativePreference: () => RelativePreference, RelativePreference: () => RelativePreference,
countPathComponents: () => countPathComponents, countPathComponents: () => countPathComponents,
forEachFileNameOfModule: () => forEachFileNameOfModule, forEachFileNameOfModule: () => forEachFileNameOfModule,
@@ -118075,7 +118106,7 @@ ${lanes.join(`
} }
} }
var ts_JsTyping_exports = {}; var ts_JsTyping_exports = {};
__export(ts_JsTyping_exports, { __export2(ts_JsTyping_exports, {
NameValidationResult: () => NameValidationResult, NameValidationResult: () => NameValidationResult,
discoverTypings: () => discoverTypings, discoverTypings: () => discoverTypings,
isTypingUpToDate: () => isTypingUpToDate, isTypingUpToDate: () => isTypingUpToDate,
@@ -121532,8 +121563,8 @@ ${lanes.join(`
} }
} }
function getIsExcludedPatterns(preferences, useCaseSensitiveFileNames2) { function getIsExcludedPatterns(preferences, useCaseSensitiveFileNames2) {
return mapDefined(preferences.autoImportFileExcludePatterns, (spec2) => { return mapDefined(preferences.autoImportFileExcludePatterns, (spec3) => {
const pattern = getSubPatternFromSpec(spec2, "", "exclude"); const pattern = getSubPatternFromSpec(spec3, "", "exclude");
return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames2) : undefined; return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames2) : undefined;
}); });
} }
@@ -124366,7 +124397,7 @@ interface Symbol {
return options; return options;
} }
var ts_NavigateTo_exports = {}; var ts_NavigateTo_exports = {};
__export(ts_NavigateTo_exports, { __export2(ts_NavigateTo_exports, {
getNavigateToItems: () => getNavigateToItems getNavigateToItems: () => getNavigateToItems
}); });
function getNavigateToItems(sourceFiles, checker, cancellationToken, searchValue, maxResultCount, excludeDtsFiles, excludeLibFiles, program) { function getNavigateToItems(sourceFiles, checker, cancellationToken, searchValue, maxResultCount, excludeDtsFiles, excludeLibFiles, program) {
@@ -124471,7 +124502,7 @@ interface Symbol {
}; };
} }
var ts_NavigationBar_exports = {}; var ts_NavigationBar_exports = {};
__export(ts_NavigationBar_exports, { __export2(ts_NavigationBar_exports, {
getNavigationBarItems: () => getNavigationBarItems, getNavigationBarItems: () => getNavigationBarItems,
getNavigationTree: () => getNavigationTree getNavigationTree: () => getNavigationTree
}); });
@@ -125200,7 +125231,7 @@ interface Symbol {
return text.replace(/\\?(?:\r?\n|[\r\u2028\u2029])/g, ""); return text.replace(/\\?(?:\r?\n|[\r\u2028\u2029])/g, "");
} }
var ts_refactor_exports = {}; var ts_refactor_exports = {};
__export(ts_refactor_exports, { __export2(ts_refactor_exports, {
addExportsInOldFile: () => addExportsInOldFile, addExportsInOldFile: () => addExportsInOldFile,
addImportsForMovedSymbols: () => addImportsForMovedSymbols, addImportsForMovedSymbols: () => addImportsForMovedSymbols,
addNewFileToTsconfig: () => addNewFileToTsconfig, addNewFileToTsconfig: () => addNewFileToTsconfig,
@@ -125345,8 +125376,8 @@ interface Symbol {
if (wasDefault) { if (wasDefault) {
if (isExportAssignment(exportNode) && !exportNode.isExportEquals) { if (isExportAssignment(exportNode) && !exportNode.isExportEquals) {
const exp = exportNode.expression; const exp = exportNode.expression;
const spec2 = makeExportSpecifier(exp.text, exp.text); const spec3 = makeExportSpecifier(exp.text, exp.text);
changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(undefined, false, factory.createNamedExports([spec2]))); changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(undefined, false, factory.createNamedExports([spec3])));
} else { } else {
changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, 90), "Should find a default keyword in modifier list")); changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, 90), "Should find a default keyword in modifier list"));
} }
@@ -125397,17 +125428,17 @@ interface Symbol {
break; break;
case 277: case 277:
case 282: { case 282: {
const spec2 = parent2; const spec3 = parent2;
changes.replaceNode(importingSourceFile, spec2, makeImportSpecifier(exportName, spec2.name.text)); changes.replaceNode(importingSourceFile, spec3, makeImportSpecifier(exportName, spec3.name.text));
break; break;
} }
case 274: { case 274: {
const clause = parent2; const clause = parent2;
Debug.assert(clause.name === ref, "Import clause name should match provided ref"); Debug.assert(clause.name === ref, "Import clause name should match provided ref");
const spec2 = makeImportSpecifier(exportName, ref.text); const spec3 = makeImportSpecifier(exportName, ref.text);
const { namedBindings } = clause; const { namedBindings } = clause;
if (!namedBindings) { if (!namedBindings) {
changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec2])); changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec3]));
} else if (namedBindings.kind === 275) { } else if (namedBindings.kind === 275) {
changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) });
const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : 1; const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : 1;
@@ -125415,7 +125446,7 @@ interface Symbol {
changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); changes.insertNodeAfter(importingSourceFile, clause.parent, newImport);
} else { } else {
changes.delete(importingSourceFile, ref); changes.delete(importingSourceFile, ref);
changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec2); changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec3);
} }
break; break;
} }
@@ -128196,7 +128227,7 @@ ${newComment.split(`
} }
} }
var ts_refactor_extractSymbol_exports = {}; var ts_refactor_extractSymbol_exports = {};
__export(ts_refactor_extractSymbol_exports, { __export2(ts_refactor_extractSymbol_exports, {
Messages: () => Messages, Messages: () => Messages,
RangeFacts: () => RangeFacts, RangeFacts: () => RangeFacts,
getRangeToExtract: () => getRangeToExtract2, getRangeToExtract: () => getRangeToExtract2,
@@ -132122,7 +132153,7 @@ ${newComment.split(`
return result; return result;
} }
var ts_BreakpointResolver_exports = {}; var ts_BreakpointResolver_exports = {};
__export(ts_BreakpointResolver_exports, { __export2(ts_BreakpointResolver_exports, {
spanInSourceFileAtLocation: () => spanInSourceFileAtLocation spanInSourceFileAtLocation: () => spanInSourceFileAtLocation
}); });
function spanInSourceFileAtLocation(sourceFile, position) { function spanInSourceFileAtLocation(sourceFile, position) {
@@ -132600,7 +132631,7 @@ ${newComment.split(`
} }
} }
var ts_CallHierarchy_exports = {}; var ts_CallHierarchy_exports = {};
__export(ts_CallHierarchy_exports, { __export2(ts_CallHierarchy_exports, {
createCallHierarchyItem: () => createCallHierarchyItem, createCallHierarchyItem: () => createCallHierarchyItem,
getIncomingCalls: () => getIncomingCalls, getIncomingCalls: () => getIncomingCalls,
getOutgoingCalls: () => getOutgoingCalls, getOutgoingCalls: () => getOutgoingCalls,
@@ -133009,11 +133040,11 @@ ${newComment.split(`
return group(collectCallSites(program, declaration), getCallSiteGroupKey, (entries) => convertCallSiteGroupToOutgoingCall(program, entries)); return group(collectCallSites(program, declaration), getCallSiteGroupKey, (entries) => convertCallSiteGroupToOutgoingCall(program, entries));
} }
var ts_classifier_exports = {}; var ts_classifier_exports = {};
__export(ts_classifier_exports, { __export2(ts_classifier_exports, {
v2020: () => ts_classifier_v2020_exports v2020: () => ts_classifier_v2020_exports
}); });
var ts_classifier_v2020_exports = {}; var ts_classifier_v2020_exports = {};
__export(ts_classifier_v2020_exports, { __export2(ts_classifier_v2020_exports, {
TokenEncodingConsts: () => TokenEncodingConsts, TokenEncodingConsts: () => TokenEncodingConsts,
TokenModifier: () => TokenModifier, TokenModifier: () => TokenModifier,
TokenType: () => TokenType, TokenType: () => TokenType,
@@ -133021,7 +133052,7 @@ ${newComment.split(`
getSemanticClassifications: () => getSemanticClassifications2 getSemanticClassifications: () => getSemanticClassifications2
}); });
var ts_codefix_exports = {}; var ts_codefix_exports = {};
__export(ts_codefix_exports, { __export2(ts_codefix_exports, {
PreserveOptionalFlags: () => PreserveOptionalFlags, PreserveOptionalFlags: () => PreserveOptionalFlags,
addNewNodeForMemberSymbol: () => addNewNodeForMemberSymbol, addNewNodeForMemberSymbol: () => addNewNodeForMemberSymbol,
codeFixAll: () => codeFixAll, codeFixAll: () => codeFixAll,
@@ -136454,13 +136485,13 @@ ${newComment.split(`
changes.replaceNode(sourceFile, clause.namedBindings, factory.updateNamedImports(clause.namedBindings, toSorted([...existingSpecifiers.filter((s) => !removeExistingImportSpecifiers.has(s)), ...newSpecifiers], specifierComparer))); changes.replaceNode(sourceFile, clause.namedBindings, factory.updateNamedImports(clause.namedBindings, toSorted([...existingSpecifiers.filter((s) => !removeExistingImportSpecifiers.has(s)), ...newSpecifiers], specifierComparer)));
} else if ((existingSpecifiers == null ? undefined : existingSpecifiers.length) && isSorted !== false) { } else if ((existingSpecifiers == null ? undefined : existingSpecifiers.length) && isSorted !== false) {
const transformedExistingSpecifiers = promoteFromTypeOnly2 && existingSpecifiers ? factory.updateNamedImports(clause.namedBindings, sameMap(existingSpecifiers, (e) => factory.updateImportSpecifier(e, true, e.propertyName, e.name))).elements : existingSpecifiers; const transformedExistingSpecifiers = promoteFromTypeOnly2 && existingSpecifiers ? factory.updateNamedImports(clause.namedBindings, sameMap(existingSpecifiers, (e) => factory.updateImportSpecifier(e, true, e.propertyName, e.name))).elements : existingSpecifiers;
for (const spec2 of newSpecifiers) { for (const spec3 of newSpecifiers) {
const insertionIndex = ts_OrganizeImports_exports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec2, specifierComparer); const insertionIndex = ts_OrganizeImports_exports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec3, specifierComparer);
changes.insertImportSpecifierAtIndex(sourceFile, spec2, clause.namedBindings, insertionIndex); changes.insertImportSpecifierAtIndex(sourceFile, spec3, clause.namedBindings, insertionIndex);
} }
} else if (existingSpecifiers == null ? undefined : existingSpecifiers.length) { } else if (existingSpecifiers == null ? undefined : existingSpecifiers.length) {
for (const spec2 of newSpecifiers) { for (const spec3 of newSpecifiers) {
changes.insertNodeInListAfter(sourceFile, last(existingSpecifiers), spec2, existingSpecifiers); changes.insertNodeInListAfter(sourceFile, last(existingSpecifiers), spec3, existingSpecifiers);
} }
} else { } else {
if (newSpecifiers.length) { if (newSpecifiers.length) {
@@ -142220,7 +142251,7 @@ ${newComment.split(`
} }
} }
var ts_Completions_exports = {}; var ts_Completions_exports = {};
__export(ts_Completions_exports, { __export2(ts_Completions_exports, {
CompletionKind: () => CompletionKind, CompletionKind: () => CompletionKind,
CompletionSource: () => CompletionSource, CompletionSource: () => CompletionSource,
SortText: () => SortText, SortText: () => SortText,
@@ -145770,7 +145801,7 @@ ${newComment.split(`
return keyword === "abstract" || keyword === "async" || keyword === "await" || keyword === "declare" || keyword === "module" || keyword === "namespace" || keyword === "type" || keyword === "satisfies" || keyword === "as"; return keyword === "abstract" || keyword === "async" || keyword === "await" || keyword === "declare" || keyword === "module" || keyword === "namespace" || keyword === "type" || keyword === "satisfies" || keyword === "as";
} }
var ts_Completions_StringCompletions_exports = {}; var ts_Completions_StringCompletions_exports = {};
__export(ts_Completions_StringCompletions_exports, { __export2(ts_Completions_StringCompletions_exports, {
getStringLiteralCompletionDetails: () => getStringLiteralCompletionDetails, getStringLiteralCompletionDetails: () => getStringLiteralCompletionDetails,
getStringLiteralCompletions: () => getStringLiteralCompletions getStringLiteralCompletions: () => getStringLiteralCompletions
}); });
@@ -146603,7 +146634,7 @@ ${newComment.split(`
return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require"; return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require";
} }
var ts_FindAllReferences_exports = {}; var ts_FindAllReferences_exports = {};
__export(ts_FindAllReferences_exports, { __export2(ts_FindAllReferences_exports, {
Core: () => Core, Core: () => Core,
DefinitionKind: () => DefinitionKind, DefinitionKind: () => DefinitionKind,
EntryKind: () => EntryKind, EntryKind: () => EntryKind,
@@ -148818,7 +148849,7 @@ ${newComment.split(`
} }
})(Core || (Core = {})); })(Core || (Core = {}));
var ts_GoToDefinition_exports = {}; var ts_GoToDefinition_exports = {};
__export(ts_GoToDefinition_exports, { __export2(ts_GoToDefinition_exports, {
createDefinitionInfo: () => createDefinitionInfo, createDefinitionInfo: () => createDefinitionInfo,
getDefinitionAndBoundSpan: () => getDefinitionAndBoundSpan, getDefinitionAndBoundSpan: () => getDefinitionAndBoundSpan,
getDefinitionAtPosition: () => getDefinitionAtPosition, getDefinitionAtPosition: () => getDefinitionAtPosition,
@@ -149314,7 +149345,7 @@ ${newComment.split(`
} }
} }
var ts_InlayHints_exports = {}; var ts_InlayHints_exports = {};
__export(ts_InlayHints_exports, { __export2(ts_InlayHints_exports, {
provideInlayHints: () => provideInlayHints provideInlayHints: () => provideInlayHints
}); });
var leadingParameterNameCommentRegexFactory = (name) => { var leadingParameterNameCommentRegexFactory = (name) => {
@@ -150076,7 +150107,7 @@ ${newComment.split(`
} }
} }
var ts_JsDoc_exports = {}; var ts_JsDoc_exports = {};
__export(ts_JsDoc_exports, { __export2(ts_JsDoc_exports, {
getDocCommentTemplateAtPosition: () => getDocCommentTemplateAtPosition, getDocCommentTemplateAtPosition: () => getDocCommentTemplateAtPosition,
getJSDocParameterNameCompletionDetails: () => getJSDocParameterNameCompletionDetails, getJSDocParameterNameCompletionDetails: () => getJSDocParameterNameCompletionDetails,
getJSDocParameterNameCompletions: () => getJSDocParameterNameCompletions, getJSDocParameterNameCompletions: () => getJSDocParameterNameCompletions,
@@ -150496,7 +150527,7 @@ ${newComment.split(`
} }
} }
var ts_MapCode_exports = {}; var ts_MapCode_exports = {};
__export(ts_MapCode_exports, { __export2(ts_MapCode_exports, {
mapCode: () => mapCode mapCode: () => mapCode
}); });
function mapCode(sourceFile, contents, focusLocations, host, formatContext, preferences) { function mapCode(sourceFile, contents, focusLocations, host, formatContext, preferences) {
@@ -150631,7 +150662,7 @@ ${content}
node.forEachChild(resetNodePositions); node.forEachChild(resetNodePositions);
} }
var ts_OrganizeImports_exports = {}; var ts_OrganizeImports_exports = {};
__export(ts_OrganizeImports_exports, { __export2(ts_OrganizeImports_exports, {
compareImportsOrRequireStatements: () => compareImportsOrRequireStatements, compareImportsOrRequireStatements: () => compareImportsOrRequireStatements,
compareModuleSpecifiers: () => compareModuleSpecifiers2, compareModuleSpecifiers: () => compareModuleSpecifiers2,
getImportDeclarationInsertionIndex: () => getImportDeclarationInsertionIndex, getImportDeclarationInsertionIndex: () => getImportDeclarationInsertionIndex,
@@ -151214,7 +151245,7 @@ ${content}
return compareModuleSpecifiersWorker(m1, m2, comparer); return compareModuleSpecifiersWorker(m1, m2, comparer);
} }
var ts_OutliningElementsCollector_exports = {}; var ts_OutliningElementsCollector_exports = {};
__export(ts_OutliningElementsCollector_exports, { __export2(ts_OutliningElementsCollector_exports, {
collectElements: () => collectElements collectElements: () => collectElements
}); });
function collectElements(sourceFile, cancellationToken) { function collectElements(sourceFile, cancellationToken) {
@@ -151532,7 +151563,7 @@ ${content}
return findChildOfKind(body, 19, sourceFile); return findChildOfKind(body, 19, sourceFile);
} }
var ts_Rename_exports = {}; var ts_Rename_exports = {};
__export(ts_Rename_exports, { __export2(ts_Rename_exports, {
getRenameInfo: () => getRenameInfo, getRenameInfo: () => getRenameInfo,
nodeIsEligibleForRename: () => nodeIsEligibleForRename nodeIsEligibleForRename: () => nodeIsEligibleForRename
}); });
@@ -151686,7 +151717,7 @@ ${content}
} }
} }
var ts_SignatureHelp_exports = {}; var ts_SignatureHelp_exports = {};
__export(ts_SignatureHelp_exports, { __export2(ts_SignatureHelp_exports, {
getArgumentInfoForCompletions: () => getArgumentInfoForCompletions, getArgumentInfoForCompletions: () => getArgumentInfoForCompletions,
getSignatureHelpItems: () => getSignatureHelpItems getSignatureHelpItems: () => getSignatureHelpItems
}); });
@@ -152160,7 +152191,7 @@ ${content}
return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false }; return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false };
} }
var ts_SmartSelectionRange_exports = {}; var ts_SmartSelectionRange_exports = {};
__export(ts_SmartSelectionRange_exports, { __export2(ts_SmartSelectionRange_exports, {
getSmartSelectionRange: () => getSmartSelectionRange getSmartSelectionRange: () => getSmartSelectionRange
}); });
function getSmartSelectionRange(pos, sourceFile) { function getSmartSelectionRange(pos, sourceFile) {
@@ -152352,7 +152383,7 @@ ${content}
} }
} }
var ts_SymbolDisplay_exports = {}; var ts_SymbolDisplay_exports = {};
__export(ts_SymbolDisplay_exports, { __export2(ts_SymbolDisplay_exports, {
getSymbolDisplayPartsDocumentationAndSymbolKind: () => getSymbolDisplayPartsDocumentationAndSymbolKind, getSymbolDisplayPartsDocumentationAndSymbolKind: () => getSymbolDisplayPartsDocumentationAndSymbolKind,
getSymbolKind: () => getSymbolKind, getSymbolKind: () => getSymbolKind,
getSymbolModifiers: () => getSymbolModifiers getSymbolModifiers: () => getSymbolModifiers
@@ -153058,7 +153089,7 @@ ${content}
}); });
} }
var ts_textChanges_exports = {}; var ts_textChanges_exports = {};
__export(ts_textChanges_exports, { __export2(ts_textChanges_exports, {
ChangeTracker: () => ChangeTracker, ChangeTracker: () => ChangeTracker,
LeadingTriviaOption: () => LeadingTriviaOption, LeadingTriviaOption: () => LeadingTriviaOption,
TrailingTriviaOption: () => TrailingTriviaOption, TrailingTriviaOption: () => TrailingTriviaOption,
@@ -154255,7 +154286,7 @@ ${options.prefix}` : `
}); });
} }
var ts_formatting_exports = {}; var ts_formatting_exports = {};
__export(ts_formatting_exports, { __export2(ts_formatting_exports, {
FormattingContext: () => FormattingContext, FormattingContext: () => FormattingContext,
FormattingRequestKind: () => FormattingRequestKind, FormattingRequestKind: () => FormattingRequestKind,
RuleAction: () => RuleAction, RuleAction: () => RuleAction,
@@ -156640,7 +156671,7 @@ ${options.prefix}` : `
} }
})(SmartIndenter || (SmartIndenter = {})); })(SmartIndenter || (SmartIndenter = {}));
var ts_preparePasteEdits_exports = {}; var ts_preparePasteEdits_exports = {};
__export(ts_preparePasteEdits_exports, { __export2(ts_preparePasteEdits_exports, {
preparePasteEdits: () => preparePasteEdits preparePasteEdits: () => preparePasteEdits
}); });
function preparePasteEdits(sourceFile, copiedFromRange, checker) { function preparePasteEdits(sourceFile, copiedFromRange, checker) {
@@ -156672,7 +156703,7 @@ ${options.prefix}` : `
return shouldProvidePasteEdits; return shouldProvidePasteEdits;
} }
var ts_PasteEdits_exports = {}; var ts_PasteEdits_exports = {};
__export(ts_PasteEdits_exports, { __export2(ts_PasteEdits_exports, {
pasteEditsProvider: () => pasteEditsProvider pasteEditsProvider: () => pasteEditsProvider
}); });
var fixId55 = "providePostPasteEdits"; var fixId55 = "providePostPasteEdits";
@@ -156763,7 +156794,7 @@ ${options.prefix}` : `
}; };
} }
var ts_exports2 = {}; var ts_exports2 = {};
__export(ts_exports2, { __export2(ts_exports2, {
ANONYMOUS: () => ANONYMOUS, ANONYMOUS: () => ANONYMOUS,
AccessFlags: () => AccessFlags, AccessFlags: () => AccessFlags,
AssertionLevel: () => AssertionLevel, AssertionLevel: () => AssertionLevel,
@@ -159104,7 +159135,7 @@ ${options.prefix}` : `
}; };
} }
var ts_server_exports3 = {}; var ts_server_exports3 = {};
__export(ts_server_exports3, { __export2(ts_server_exports3, {
ActionInvalidate: () => ActionInvalidate, ActionInvalidate: () => ActionInvalidate,
ActionPackageInstalled: () => ActionPackageInstalled, ActionPackageInstalled: () => ActionPackageInstalled,
ActionSet: () => ActionSet, ActionSet: () => ActionSet,
@@ -159202,7 +159233,7 @@ ${options.prefix}` : `
updateProjectIfDirty: () => updateProjectIfDirty updateProjectIfDirty: () => updateProjectIfDirty
}); });
var ts_server_typingsInstaller_exports = {}; var ts_server_typingsInstaller_exports = {};
__export(ts_server_typingsInstaller_exports, { __export2(ts_server_typingsInstaller_exports, {
TypingsInstaller: () => TypingsInstaller, TypingsInstaller: () => TypingsInstaller,
getNpmCommandForInstallation: () => getNpmCommandForInstallation, getNpmCommandForInstallation: () => getNpmCommandForInstallation,
installNpmPackages: () => installNpmPackages, installNpmPackages: () => installNpmPackages,
@@ -159757,7 +159788,7 @@ ${options.prefix}` : `
return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined; return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined;
} }
var ts_server_protocol_exports = {}; var ts_server_protocol_exports = {};
__export(ts_server_protocol_exports, { __export2(ts_server_protocol_exports, {
ClassificationType: () => ClassificationType, ClassificationType: () => ClassificationType,
CommandTypes: () => CommandTypes, CommandTypes: () => CommandTypes,
CompletionTriggerKind: () => CompletionTriggerKind, CompletionTriggerKind: () => CompletionTriggerKind,
@@ -169359,7 +169390,7 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
_TypingsInstallerAdapter.requestDelayMillis = 100; _TypingsInstallerAdapter.requestDelayMillis = 100;
var TypingsInstallerAdapter = _TypingsInstallerAdapter; var TypingsInstallerAdapter = _TypingsInstallerAdapter;
var ts_server_exports4 = {}; var ts_server_exports4 = {};
__export(ts_server_exports4, { __export2(ts_server_exports4, {
ActionInvalidate: () => ActionInvalidate, ActionInvalidate: () => ActionInvalidate,
ActionPackageInstalled: () => ActionPackageInstalled, ActionPackageInstalled: () => ActionPackageInstalled,
ActionSet: () => ActionSet, ActionSet: () => ActionSet,
@@ -169485,10 +169516,10 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js // node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
var require_main = __commonJS((exports2, module2) => { var require_main = __commonJS((exports2, module2) => {
var __defProp2 = Object.defineProperty; var __defProp2 = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
var __hasOwnProp2 = Object.prototype.hasOwnProperty; var __hasOwnProp2 = Object.prototype.hasOwnProperty;
var __export = (target, all) => { var __export2 = (target, all) => {
for (var name in all) for (var name in all)
__defProp2(target, name, { get: all[name], enumerable: true }); __defProp2(target, name, { get: all[name], enumerable: true });
}; };
@@ -169496,17 +169527,17 @@ var require_main = __commonJS((exports2, module2) => {
if (from && typeof from === "object" || typeof from === "function") { if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames2(from)) for (let key of __getOwnPropNames2(from))
if (!__hasOwnProp2.call(to, key) && key !== except) if (!__hasOwnProp2.call(to, key) && key !== except)
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
} }
return to; return to;
}; };
var __toCommonJS = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod); var __toCommonJS2 = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod);
var main_exports = {}; var main_exports = {};
__export(main_exports, { __export2(main_exports, {
config: () => config, config: () => config,
t: () => t t: () => t
}); });
module2.exports = __toCommonJS(main_exports); module2.exports = __toCommonJS2(main_exports);
var import_fs = require("fs"); var import_fs = require("fs");
var import_promises = require("fs/promises"); var import_promises = require("fs/promises");
async function readFileFromUri(uri) { async function readFileFromUri(uri) {
@@ -169624,6 +169655,14 @@ var require_main = __commonJS((exports2, module2) => {
} }
}); });
// packages/language-server/src/server.ts
var exports_server = {};
__export(exports_server, {
apiCallHover: () => apiCallHover,
apiCallCompletions: () => apiCallCompletions
});
module.exports = __toCommonJS(exports_server);
// packages/syntax/src/tokenizer.ts // packages/syntax/src/tokenizer.ts
class LexError extends Error { class LexError extends Error {
} }
@@ -196164,6 +196203,46 @@ function clearHtmlRegionCache(uri) {
} }
// packages/language-server/src/server.ts // packages/language-server/src/server.ts
function dataApisOf(source) {
try {
return parse(source).dataApis;
} catch {
return [];
}
}
function apiDetail(block) {
return `${block.method} ${block.path}`;
}
function apiCallCompletions(source) {
return dataApisOf(source).map((block) => ({
label: block.name,
kind: 2,
detail: apiDetail(block),
documentation: block.sections ? [
block.sections.body.length ? `body: ${block.sections.body.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}` : undefined
].filter(Boolean).join(`
`) : undefined
}));
}
function apiCallHover(source, name) {
const block = dataApisOf(source).find((entry) => entry.name === name);
if (!block)
return;
const lines = [`**${block.name}** \`${block.method} ${block.path}\``];
if (block.sections?.body.length) {
lines.push(`request body: ${block.sections.body.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}`);
}
if (block.sections?.parameters.length) {
lines.push(`parameters: ${block.sections.parameters.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}`);
}
return lines.join(`
`);
}
function isApiCallPosition(text, offset) {
const before = text.slice(0, offset);
return /\bapi\.\w*$/.test(before);
}
var documents = new Map; var documents = new Map;
var diagnosticTimers = new Map; var diagnosticTimers = new Map;
var MAX_OPEN_DOCUMENTS = 256; var MAX_OPEN_DOCUMENTS = 256;
@@ -196336,6 +196415,10 @@ async function handle(message) {
} }
case "textDocument/completion": { case "textDocument/completion": {
const document = documents.get(params.textDocument.uri); const document = documents.get(params.textDocument.uri);
if (document && isApiCallPosition(document.text, offsetAt(document.text, params.position))) {
result(message.id, apiCallCompletions(document.text));
break;
}
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)]; const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
const html = document ? htmlCompletions(document, params.position) : []; const html = document ? htmlCompletions(document, params.position) : [];
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus); result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
@@ -196353,6 +196436,12 @@ async function handle(message) {
} }
case "textDocument/hover": { case "textDocument/hover": {
const document = documents.get(params.textDocument.uri); const document = documents.get(params.textDocument.uri);
const word = document ? wordAt(document.text, params.position)?.word : undefined;
const apiHover = word ? apiCallHover(document.text, word) : undefined;
if (apiHover) {
result(message.id, { contents: apiHover });
break;
}
const html = document ? htmlHover(document, params.position) : null; const html = document ? htmlHover(document, params.position) : null;
result(message.id, html ?? (document ? hover(document, params.position) : null)); result(message.id, html ?? (document ? hover(document, params.position) : null));
break; break;
-3
View File
@@ -106,9 +106,6 @@ test('REGRESSION: runtime = "client" still highlights via execution-decl', () =>
test("REGRESSION: the client function modifier still highlights via functions-block", () => { test("REGRESSION: the client function modifier still highlights via functions-block", () => {
const rule = ruleByTopLevelBlockName("functions-block"); const rule = ruleByTopLevelBlockName("functions-block");
const modifierPattern = rule.patterns.find(
(p) => (p.match && /storage\.modifier\.async/.test("")) || p.match,
);
const matchRe = new RegExp( const matchRe = new RegExp(
rule.patterns.find( rule.patterns.find(
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn", (p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
+5
View File
@@ -0,0 +1,5 @@
dist/
.wrnexus/
*.sqlite
*.sqlite-shm
*.sqlite-wal
+13
View File
@@ -0,0 +1,13 @@
# Northstar CRM example
A complete WrNexus example covering public pages, SQLite migrations and seed data, SQL-backed signup/login/session authentication, database-backed roles and permissions, protected pages, and permission-checked CRM APIs.
```bash
bun run db:migrate
bun run db:seed
bun run dev
```
Open `/signup`, create an account, and the signup hook assigns the `sales-rep` role. The user is automatically signed in and redirected to `/dashboard`. Auth tables come from the `@wrnexus/auth` plugin migrations; CRM and authorization tables come from `app/db/migrations/001_crm.sql`.
The seed records use the placeholder owner `demo-owner` to demonstrate repeatable data. To attach them to a registered user, update their `owner_id` to that user's ID.
+35
View File
@@ -0,0 +1,35 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
const userId = (ctx: Context) => String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "contact:read")))
return Response.json({ error: "Forbidden" }, { status: 403 });
await ensureCrmWorkspace(userId(ctx));
const rows = await getDb().all(
"SELECT id,name,email,company,phone,status,created_at FROM crm_contacts WHERE owner_id = ? ORDER BY name",
[userId(ctx)],
);
return Response.json({ contacts: rows });
}
export async function POST(ctx: Context): Promise<Response> {
if (!(await can(ctx, "contact:write")))
return Response.json({ error: "Forbidden" }, { status: 403 });
const body = (await ctx.req.json()) as Record<string, unknown>;
const name = String(body.name ?? "").trim();
const email = String(body.email ?? "")
.trim()
.toLowerCase();
if (!name || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return Response.json({ error: "A name and valid email are required" }, { status: 400 });
}
const result = await getDb().exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[userId(ctx), name, email, String(body.company ?? ""), String(body.phone ?? ""), "lead"],
);
return Response.json({ id: Number(result.lastInsertId), name, email }, { status: 201 });
}
+33
View File
@@ -0,0 +1,33 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "crm:dashboard")))
return Response.json({ error: "Forbidden" }, { status: 403 });
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
await ensureCrmWorkspace(owner);
const metrics = await getDb().one<{
pipeline_value_cents: number;
open_opportunities: number;
contacts: number;
}>(
`SELECT
COALESCE((SELECT SUM(value_cents) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')), 0) pipeline_value_cents,
(SELECT COUNT(*) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')) open_opportunities,
(SELECT COUNT(*) FROM crm_contacts WHERE owner_id = ?) contacts`,
[owner, owner, owner],
);
return Response.json({
metrics: {
pipelineValue: new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(Number(metrics?.pipeline_value_cents ?? 0) / 100),
openOpportunities: Number(metrics?.open_opportunities ?? 0),
contacts: Number(metrics?.contacts ?? 0),
},
});
}
+15
View File
@@ -0,0 +1,15 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "deal:read"))) return Response.json({ error: "Forbidden" }, { status: 403 });
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
await ensureCrmWorkspace(owner);
const deals = await getDb().all(
"SELECT d.id,d.title,d.value_cents,d.stage,d.close_date,c.name contact_name FROM crm_deals d LEFT JOIN crm_contacts c ON c.id=d.contact_id WHERE d.owner_id=? ORDER BY d.updated_at DESC",
[owner],
);
return Response.json({ deals });
}
+19
View File
@@ -0,0 +1,19 @@
import { defineAuthz } from "@wrnexus/authz";
export default defineAuthz({
permissions: {
"crm:dashboard": { title: "View dashboard" },
"contact:read": { title: "View contacts" },
"contact:write": { title: "Create and edit contacts" },
"contact:delete": { title: "Delete contacts", risk: "high" },
"deal:read": { title: "View deals" },
"deal:write": { title: "Create and edit deals" },
"admin:access": { title: "Manage CRM access", risk: "high" },
},
roles: {
viewer: ["crm:dashboard", "contact:read", "deal:read"],
"sales-rep": ["role:viewer", "contact:write", "deal:write"],
manager: ["role:sales-rep", "contact:delete"],
admin: ["role:manager", "admin:access"],
},
});
@@ -0,0 +1,65 @@
-- +up
CREATE TABLE IF NOT EXISTS crm_contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
company TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'lead' CHECK (status IN ('lead', 'customer', 'inactive')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_contacts_owner_idx ON crm_contacts(owner_id);
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_owner_email_uq ON crm_contacts(owner_id, email);
CREATE TABLE IF NOT EXISTS crm_deals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
contact_id INTEGER REFERENCES crm_contacts(id) ON DELETE SET NULL,
title TEXT NOT NULL,
value_cents INTEGER NOT NULL DEFAULT 0 CHECK (value_cents >= 0),
stage TEXT NOT NULL DEFAULT 'qualified' CHECK (stage IN ('qualified', 'proposal', 'won', 'lost')),
close_date TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_deals_owner_idx ON crm_deals(owner_id);
CREATE INDEX IF NOT EXISTS crm_deals_contact_idx ON crm_deals(contact_id);
CREATE TABLE IF NOT EXISTS crm_activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL,
entity_type TEXT NOT NULL CHECK (entity_type IN ('contact', 'deal', 'account')),
entity_id TEXT NOT NULL,
action TEXT NOT NULL,
details_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_activities_entity_idx ON crm_activities(entity_type, entity_id);
-- Database-backed authorization assignments.
CREATE TABLE IF NOT EXISTS _wrn_authz_assignment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id VARCHAR(255) NOT NULL,
scope VARCHAR(255) NOT NULL DEFAULT '',
role VARCHAR(255) NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT _wrn_authz_assignment_unique UNIQUE (subject_id, scope, role)
);
CREATE TABLE IF NOT EXISTS _wrn_authz_grant (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id VARCHAR(255) NOT NULL,
scope VARCHAR(255) NOT NULL DEFAULT '',
permission VARCHAR(255) NOT NULL,
effect VARCHAR(16) NOT NULL CHECK (effect IN ('allow', 'deny')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT _wrn_authz_grant_unique UNIQUE (subject_id, scope, permission)
);
-- +down
DROP TABLE IF EXISTS _wrn_authz_grant;
DROP TABLE IF EXISTS _wrn_authz_assignment;
DROP TABLE IF EXISTS crm_activities;
DROP TABLE IF EXISTS crm_deals;
DROP TABLE IF EXISTS crm_contacts;
+23
View File
@@ -0,0 +1,23 @@
import type { Db } from "@wrnexus/db";
export default async function seed(db: Db): Promise<void> {
const owner = "demo-owner";
await db.exec("DELETE FROM crm_activities");
await db.exec("DELETE FROM crm_deals");
await db.exec("DELETE FROM crm_contacts");
await db.exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[owner, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
);
await db.exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[owner, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
);
const contact = await db.one<{ id: number }>("SELECT id FROM crm_contacts WHERE email = ?", [
"ada@example.test",
]);
await db.exec(
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
[owner, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
);
}
+32
View File
@@ -0,0 +1,32 @@
import { createAuthEngine, SqlAuthStore, type AuthStore } from "@wrnexus/auth";
import { getDb } from "@wrnexus/db";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { ensureCrmWorkspace } from "./workspace.ts";
// Config is imported before the runtime opens its configured database. This
// forwarding store resolves getDb() only when an auth operation actually runs.
const store = new Proxy({} as AuthStore, {
get(_target, property) {
const value = Reflect.get(new SqlAuthStore(getDb()), property);
return typeof value === "function" ? value.bind(new SqlAuthStore(getDb())) : value;
},
});
export const auth = createAuthEngine({
store,
secret: process.env.AUTH_SECRET ?? "northstar-crm-development-secret-change-me",
issuer: "Northstar CRM",
onSignedIn(ctx, returnTo) {
const destination =
returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/dashboard";
return Response.redirect(new URL(destination, ctx.url), 303);
},
onSignedOut(ctx) {
return Response.redirect(new URL("/", ctx.url), 303);
},
async onSuccessfulSignUp(_ctx, user) {
await dbPermissionStore(getDb()).assignRole(user.id, "sales-rep");
await ensureCrmWorkspace(user.id);
return { autoSignIn: true, redirectTo: "/dashboard" };
},
});
+29
View File
@@ -0,0 +1,29 @@
import { getDb } from "@wrnexus/db";
/** Give every real CRM user a useful workspace on first access. */
export async function ensureCrmWorkspace(ownerId: string): Promise<void> {
if (!ownerId) return;
const db = getDb();
await db.exec(
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[ownerId, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
);
await db.exec(
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[ownerId, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
);
const existingDeal = await db.one<{ count: number }>(
"SELECT COUNT(*) count FROM crm_deals WHERE owner_id = ?",
[ownerId],
);
if (Number(existingDeal?.count ?? 0) === 0) {
const contact = await db.one<{ id: number }>(
"SELECT id FROM crm_contacts WHERE owner_id = ? AND email = ?",
[ownerId, "ada@example.test"],
);
await db.exec(
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
[ownerId, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import { dbPermissionStore } from "@wrnexus/authz/db";
import type { Middleware } from "@wrnexus/core";
// Production imports middleware before it opens configured databases. Resolve
// the SQL-backed store on the first request, after runtime initialization.
const middleware: Middleware = (ctx, next) =>
authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) })(ctx, next);
export default middleware;
@@ -0,0 +1,18 @@
import { requireAuth } from "@wrnexus/auth";
import { can } from "@wrnexus/authz";
import type { Middleware } from "@wrnexus/core";
const guard = requireAuth({ loginPath: "/login" });
const protectedPrefixes = ["/dashboard", "/contacts", "/deals", "/admin"];
const middleware: Middleware = (ctx, next) => {
if (!protectedPrefixes.some((prefix) => ctx.url.pathname.startsWith(prefix))) return next();
return guard(ctx, async () => {
if (ctx.url.pathname.startsWith("/admin") && !(await can(ctx, "admin:access"))) {
return Response.redirect(new URL("/forbidden", ctx.url), 303);
}
return next();
});
};
export default middleware;
+5
View File
@@ -0,0 +1,5 @@
page Admin {
state user = ctx.user
seo { title = "Administration" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin" aria-current="page">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Access administration</h1><p>Manage roles and protect high-risk operations.</p></div></header><section class="crm-panel"><span class="crm-eyebrow">Role-based access</span><h2>Signed in as {user.displayName || user.username}</h2><p class="crm-hero-copy">This route requires the <strong>admin:access</strong> permission. Assign manager and administrator roles through the database-backed authorization store.</p></section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Contacts {
apis { listContacts GET /api/contacts { response { return data.contacts } } }
load server contacts { return await api.listContacts() }
seo { title = "Contacts" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts" aria-current="page">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Contacts</h1><p>Every relationship, scoped securely to its owner.</p></div><a class="crm-button" href="mailto:sales@example.test">New contact</a></header><section class="crm-panel"><span class="crm-eyebrow">Customer directory</span>{#if contacts.length}<ul class="crm-list">{#each contacts as contact}<li><strong>{contact.name}</strong><span>{contact.company || contact.email}</span><span class="crm-pill">{contact.status}</span></li>{/each}</ul>{/if}{#if !contacts.length}<div class="crm-empty"><h2>No contacts yet</h2><p>Your first customer relationship will appear here.</p></div>{/if}</section></section></main> }
}
+8
View File
@@ -0,0 +1,8 @@
page Dashboard {
state user = ctx.user
state dashboardMetrics = ctx.metrics
apis { dashboard GET /api/dashboard { response { return data.metrics } } }
load server metrics { return await api.dashboard() }
seo { title = "Dashboard" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard" aria-current="page">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav><form class="crm-sidebar-footer" method="post" action="/api/auth/logout" data-schema="auth-empty"><button class="crm-button crm-button--ghost" type="submit">Log out</button></form></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Good to see you, {user.displayName || user.username}</h1><p>Live figures from your private SQLite workspace.</p></div><a class="crm-button" href="/contacts">View contacts</a></header><div class="crm-stat-grid"><article class="crm-stat"><span>Pipeline value</span><strong>{dashboardMetrics.pipelineValue}</strong></article><article class="crm-stat"><span>Open opportunities</span><strong>{dashboardMetrics.openOpportunities}</strong></article><article class="crm-stat"><span>Total contacts</span><strong>{dashboardMetrics.contacts}</strong></article></div><section class="crm-panel"><span class="crm-eyebrow">Server rendered</span><h2>One secure data path</h2><p class="crm-hero-copy">This page was rendered from authenticated API results on the server. No browser prefetch or client data request is needed to show these metrics.</p></section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Deals {
apis { listDeals GET /api/deals { response { return data.deals } } }
load server deals { return await api.listDeals() }
seo { title = "Deals" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals" aria-current="page">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Sales pipeline</h1><p>Focus on the opportunities most likely to move.</p></div><a class="crm-button" href="/contacts">New deal</a></header><section class="crm-panel"><span class="crm-eyebrow">Open opportunities</span>{#if deals.length}<ul class="crm-list">{#each deals as deal}<li><strong>{deal.title}</strong><span>{deal.contact_name || "Unassigned contact"}</span><span class="crm-pill">{deal.stage}</span></li>{/each}</ul>{/if}{#if !deals.length}<div class="crm-empty"><h2>No open deals</h2><p>Create a contact and turn the relationship into an opportunity.</p></div>{/if}</section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Forbidden {
seo { title = "Access denied" }
view {
<main class="crm-status-page"><section class="crm-status-card"><span class="crm-status-icon" aria-hidden="true">!</span><span class="crm-eyebrow">Permission required</span><h1>You do not have access to administration.</h1><p>Your account is working correctly, but an administrator role is required for this page.</p><div class="crm-actions"><a class="crm-button" href="/dashboard">Back to dashboard</a><a class="crm-button crm-button--ghost" href="/contacts">View contacts</a></div></section></main>
}
}
+31
View File
@@ -0,0 +1,31 @@
page Home {
seo { title = "CRM that keeps sales moving" }
view {
<main
class="crm-public"
>
<nav
class="crm-nav"
>
<a
class="crm-brand"
href="/"
>
<span
class="crm-brand-mark"
>
N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/pricing">Pricing</a><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div>
</nav>
<section
class="crm-hero"
>
<div>
<span
class="crm-eyebrow"
>
Customer intelligence, simplified</span><h1>Know every customer. Close every opportunity.</h1><p class="crm-hero-copy">Bring contacts, conversations, and pipeline into one calm workspace built for teams that value clarity.</p><div class="crm-actions"><a class="crm-button" href="/signup">Create your workspace</a><a class="crm-button crm-button--ghost" href="/login">View the CRM</a></div></div><div class="crm-preview"><div class="crm-preview-bar"><span></span><span></span><span></span></div><div class="crm-preview-grid"><article class="crm-preview-card"><small>Pipeline value</small><strong>$125k</strong><small>+18% this month</small></article><article class="crm-preview-card"><small>Active contacts</small><strong>248</strong><small>32 need follow-up</small></article><article class="crm-preview-card"><small>Win rate</small><strong>42%</strong><small>Across qualified deals</small></article><article class="crm-preview-card"><small>Next action</small><strong>8 today</strong><small>Stay ahead of every promise</small></article></div>
</div>
</section>
</main>
}
}
+10
View File
@@ -0,0 +1,10 @@
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
page Login {
seo { title = "Log in" }
view {
<AuthSplitLayout brand="Northstar CRM" eyebrow="Customer intelligence" title="Turn every conversation into momentum." description="Sign in to a focused workspace for contacts, pipeline, and activity." features='[{"label":"One customer timeline","description":"Every relationship and opportunity in context."},{"label":"Secure by default","description":"SQL sessions and role-based authorization."},{"label":"Built for focus","description":"A calm workspace without sales-tool clutter."}]'>
<div data-slot="form"><SignIn action="/api/auth/login" returnTo="/dashboard" signUpHref="/signup" forgotHref="/login" showPasskey="false" class="border-0 shadow-none" /></div>
</AuthSplitLayout>
}
}
+4
View File
@@ -0,0 +1,4 @@
page Pricing {
seo { title = "Pricing" }
view { <main class="crm-public"><nav class="crm-nav"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div></nav><section class="crm-hero"><div><span class="crm-eyebrow">Simple pricing</span><h1>Start free. Scale when your team does.</h1><p class="crm-hero-copy">Everything needed to evaluate a secure, full-stack CRM locally. Move to Team when you are ready to collaborate.</p></div><div class="crm-preview"><article class="crm-preview-card"><small>Starter</small><strong>$0</strong><p>SQLite workspace, CRM flows, authentication and permissions.</p><a class="crm-button" href="/signup">Start building</a></article><article class="crm-preview-card"><small>Team</small><strong>$29</strong><p>Per user/month with shared pipeline and administration.</p></article></div></section></main> }
}
+10
View File
@@ -0,0 +1,10 @@
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
page Signup {
seo { title = "Create account" }
view {
<AuthSplitLayout brand="Northstar CRM" eyebrow="Start in minutes" title="Build relationships, not spreadsheets." description="Create your secure sales workspace and begin with a complete customer view." features='[{"label":"Free local workspace","description":"Explore the complete CRM flow with SQLite."},{"label":"Flexible permissions","description":"Viewer, sales, manager, and admin roles."},{"label":"Your data stays yours","description":"Portable SQL data and explicit migrations."}]'>
<div data-slot="form"><SignUp redirect="/dashboard" signInHref="/login" showPhone="false" showUsername="false" requireConsent="false" class="border-0 shadow-none" /></div>
</AuthSplitLayout>
}
}
+116
View File
@@ -0,0 +1,116 @@
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
"/admin": Record<string, never>;
"/contacts": Record<string, never>;
"/dashboard": Record<string, never>;
"/deals": Record<string, never>;
"/forbidden": Record<string, never>;
"/login": Record<string, never>;
"/pricing": Record<string, never>;
"/signup": Record<string, never>;
}
export interface RouteNames {
"index": "/";
"admin": "/admin";
"contacts": "/contacts";
"dashboard": "/dashboard";
"deals": "/deals";
"forbidden": "/forbidden";
"login": "/login";
"pricing": "/pricing";
"signup": "/signup";
}
export interface RouteQueries {
[path: string]: Record<string, string | number | boolean | null | undefined>;
}
export type RoutePath = keyof Routes;
export type RouteName = keyof RouteNames;
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
? RouteQueries[P]
: Record<string, string | number | boolean | null | undefined>;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
const output: string[] = [];
for (const segment of path.split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
return buildHref(String(path), params);
}
export function route<N extends RouteName>(
name: N,
...args: keyof Routes[RouteNames[N]] extends never
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: Record<string, never> extends Routes[RouteNames[N]]
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
): string {
const paths: Record<RouteName, RoutePath> = {
"index": "/",
"admin": "/admin",
"contacts": "/contacts",
"dashboard": "/dashboard",
"deals": "/deals",
"forbidden": "/forbidden",
"login": "/login",
"pricing": "/pricing",
"signup": "/signup"
} as Record<RouteName, RoutePath>;
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
const query = args[1];
if (!query) return output;
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query))
if (value !== undefined && value !== null) search.set(key, String(value));
const text = search.toString();
return text ? `${output}?${text}` : output;
}
+375
View File
@@ -0,0 +1,375 @@
@import "tailwindcss";
@plugin "@iconify/tailwind4";
@source "../**/*.wrn";
@source "../../../packages/auth/components/*.wrn";
@source "../../../packages/ui/components/*.wrn";
:root {
color-scheme: light;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--wrn-color-bg);
}
body {
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font-family: Inter, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration: none;
}
.crm-public {
min-height: 100dvh;
overflow: hidden;
background:
radial-gradient(
circle at 80% 10%,
color-mix(in srgb, var(--wrn-color-primary) 15%, transparent),
transparent 28%
),
var(--wrn-color-bg);
}
.crm-nav {
width: min(1180px, calc(100% - 2rem));
margin: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0;
}
.crm-brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
font-weight: 800;
letter-spacing: -0.025em;
}
.crm-brand-mark {
display: grid;
width: 2.25rem;
height: 2.25rem;
place-items: center;
border-radius: 0.75rem;
color: white;
background: linear-gradient(135deg, var(--wrn-color-primary), #7c3aed);
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 28%, transparent);
}
.crm-nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
color: var(--wrn-color-muted);
font-size: 0.9rem;
font-weight: 600;
}
.crm-button {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
padding: 0 1.1rem;
border-radius: 0.75rem;
color: white;
background: var(--wrn-color-primary);
font-weight: 700;
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 22%, transparent);
}
.crm-button--ghost {
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
border: 1px solid var(--wrn-color-border);
box-shadow: none;
}
.crm-hero {
width: min(1180px, calc(100% - 2rem));
margin: auto;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
align-items: center;
gap: 4rem;
padding: clamp(4rem, 9vw, 8rem) 0;
}
.crm-eyebrow {
color: var(--wrn-color-primary);
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.crm-hero h1 {
max-width: 12ch;
margin: 1rem 0;
font-size: clamp(3rem, 7vw, 5.5rem);
line-height: 0.98;
letter-spacing: -0.065em;
}
.crm-hero-copy {
max-width: 38rem;
color: var(--wrn-color-muted);
font-size: 1.1rem;
line-height: 1.75;
}
.crm-actions {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
margin-top: 2rem;
}
.crm-preview {
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1.5rem;
background: color-mix(in srgb, var(--wrn-color-surface) 88%, transparent);
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.15);
transform: rotate(1.5deg);
}
.crm-preview-bar {
display: flex;
gap: 0.4rem;
padding: 0.4rem 0.25rem 1rem;
}
.crm-preview-bar span {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: var(--wrn-color-border);
}
.crm-preview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.8rem;
}
.crm-preview-card {
min-height: 8rem;
padding: 1rem;
border-radius: 1rem;
background: var(--wrn-color-surface-2);
}
.crm-preview-card strong {
display: block;
margin-top: 0.7rem;
font-size: 1.6rem;
}
.crm-preview-card small {
color: var(--wrn-color-muted);
}
.crm-shell {
min-height: 100dvh;
display: grid;
grid-template-columns: 16rem 1fr;
background: var(--wrn-color-bg);
}
.crm-sidebar {
position: sticky;
top: 0;
height: 100dvh;
display: flex;
flex-direction: column;
padding: 1.35rem;
border-right: 1px solid var(--wrn-color-border);
background: var(--wrn-color-surface);
}
.crm-menu {
display: grid;
gap: 0.35rem;
margin-top: 2rem;
}
.crm-menu a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0.85rem;
border-radius: 0.75rem;
color: var(--wrn-color-muted);
font-size: 0.9rem;
font-weight: 650;
}
.crm-menu a:hover,
.crm-menu a[aria-current="page"] {
color: var(--wrn-color-primary);
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
}
.crm-sidebar-footer {
margin-top: auto;
}
.crm-content {
min-width: 0;
padding: clamp(1.25rem, 4vw, 3rem);
}
.crm-topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
}
.crm-topbar h1 {
margin: 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
letter-spacing: -0.04em;
}
.crm-topbar p {
margin: 0.4rem 0 0;
color: var(--wrn-color-muted);
}
.crm-panel {
padding: 1.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.crm-stat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.crm-stat {
padding: 1.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface);
}
.crm-stat span {
color: var(--wrn-color-muted);
font-size: 0.8rem;
font-weight: 650;
}
.crm-stat strong {
display: block;
margin-top: 0.6rem;
font-size: 2rem;
letter-spacing: -0.04em;
}
.crm-list {
display: grid;
gap: 0.65rem;
padding: 0;
margin: 1rem 0 0;
list-style: none;
}
.crm-list li {
display: grid;
grid-template-columns: minmax(10rem, 1fr) minmax(8rem, 0.7fr) auto;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.8rem;
background: var(--wrn-color-surface-2);
}
.crm-pill {
justify-self: end;
padding: 0.3rem 0.65rem;
border-radius: 999px;
color: var(--wrn-color-primary);
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
font-size: 0.72rem;
font-weight: 750;
text-transform: capitalize;
}
.crm-empty {
padding: 3rem 1rem;
text-align: center;
color: var(--wrn-color-muted);
}
.crm-empty h2 {
margin-bottom: 0.35rem;
color: var(--wrn-color-text);
}
.crm-status-page {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 1.5rem;
background: radial-gradient(
circle at 50% 0%,
color-mix(in srgb, var(--wrn-color-primary) 12%, transparent),
transparent 45%
);
}
.crm-status-card {
width: min(34rem, 100%);
padding: clamp(1.5rem, 5vw, 3rem);
border: 1px solid var(--wrn-color-border);
border-radius: 1.25rem;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-2);
}
.crm-status-card h1 {
margin: 0.9rem 0 0.7rem;
font-size: clamp(1.8rem, 5vw, 2.5rem);
line-height: 1.08;
letter-spacing: -0.045em;
}
.crm-status-card > p {
color: var(--wrn-color-muted);
line-height: 1.65;
}
.crm-status-icon {
display: grid;
width: 3rem;
height: 3rem;
margin-bottom: 1.25rem;
place-items: center;
border-radius: 1rem;
color: #b45309;
background: #fef3c7;
font-size: 1.4rem;
font-weight: 800;
}
@media (max-width: 820px) {
.crm-nav-links > a:not(.crm-button) {
display: none;
}
.crm-hero {
grid-template-columns: 1fr;
padding-top: 3rem;
}
.crm-preview {
transform: none;
}
.crm-shell {
grid-template-columns: 1fr;
}
.crm-sidebar {
position: static;
width: 100%;
height: auto;
}
.crm-menu {
grid-template-columns: repeat(4, 1fr);
overflow: auto;
}
.crm-sidebar-footer {
margin-top: 1rem;
}
.crm-stat-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.crm-hero h1 {
font-size: 3rem;
}
.crm-menu {
grid-template-columns: repeat(2, 1fr);
}
.crm-list li {
grid-template-columns: 1fr;
}
.crm-pill {
justify-self: start;
}
}
@@ -0,0 +1,7 @@
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
//
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
// checked; this file is compiled and checked normally by the project's own tsc.
export {};
+47
View File
@@ -0,0 +1,47 @@
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
declare namespace WRNexusGenerated {
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
? { input: I; output: O }
: T extends (...args: infer A) => infer R
? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
: { input: unknown; output: unknown };
type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
? { args: A; result: Awaited<R> }
: T extends (db: any, ...rest: any[]) => infer R
? { args: Record<string, never>; result: Awaited<R> }
: never;
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
type RouteName = "admin" | "contacts" | "dashboard" | "deals" | "forbidden" | "index" | "login" | "pricing" | "signup";
type ApiRoute = "/api/contacts" | "/api/dashboard" | "/api/deals";
type RealtimeRoute = never;
type EnvironmentKey = never;
type TranslationKey = never;
type QueueName = never;
type CacheKey = never;
type Components = Record<string, never>;
interface ApiContracts {
"/api/dashboard": { GET: ApiContract<typeof import("../api/dashboard.ts")["GET"]> };
"/api/contacts": { GET: ApiContract<typeof import("../api/contacts.ts")["GET"]>; POST: ApiContract<typeof import("../api/contacts.ts")["POST"]> };
"/api/deals": { GET: ApiContract<typeof import("../api/deals.ts")["GET"]> };
}
interface MiddlewareContexts {
"authz": MiddlewareContext<(typeof import("../middleware/authz.ts"))["default"]>;
"protected": MiddlewareContext<(typeof import("../middleware/protected.ts"))["default"]>;
}
type DatabaseQueries = Record<string, never>;
type RealtimeMessages = Record<string, never>;
type QueuePayloads = Record<string, never>;
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
type AssertAssignable<Actual, Expected> = unknown extends Expected
? true
: [Actual] extends [Expected]
? [Exclude<keyof Actual, keyof Expected>] extends [never]
? true
: false
: false;
type __wrn_expect_true<T extends true> = T;
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
}
@@ -0,0 +1 @@
// AUTO-GENERATED plugin type aggregation - do not edit.
+30
View File
@@ -0,0 +1,30 @@
{
"name": "wrnexus-crm-example",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run ../../packages/cli/src/index.ts dev .",
"build": "bun run ../../packages/cli/src/index.ts build .",
"db:migrate": "bun run ../../packages/cli/src/index.ts db migrate .",
"db:seed": "bun run ../../packages/cli/src/index.ts db seed .",
"test": "bun test test",
"typecheck": "tsc --noEmit -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/styles": "workspace:*"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.123",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3"
}
}
+82
View File
@@ -0,0 +1,82 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createDb, loadMigrations, migrate } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
import seed from "../app/db/seed.ts";
const root = join(import.meta.dir, "..");
const source = (path: string) => readFileSync(join(root, path), "utf8");
test("the CRM migration applies to SQLite, is idempotent, and the seed is repeatable", async () => {
const db = createDb(sqlite());
const migrations = join(root, "app", "db", "migrations");
expect(loadMigrations(migrations).map(({ name }) => name)).toEqual(["001_crm"]);
expect(await migrate(db, migrations)).toEqual(["001_crm"]);
expect(await migrate(db, migrations)).toEqual([]);
await seed(db);
await seed(db);
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_contacts")).toEqual({
count: 2,
});
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_deals")).toEqual({
count: 1,
});
expect(
await db.all(
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('_wrn_authz_assignment','_wrn_authz_grant') ORDER BY name",
),
).toHaveLength(2);
await db.close();
});
test("authentication uses SQL, plugin migrations, signup/login components, and protected routes", () => {
expect(source("app/lib/auth.ts")).toContain("SqlAuthStore");
expect(source("wrnexus.config.ts")).toContain("migrations: true");
expect(source("app/pages/login.wrn")).toContain("<SignIn");
expect(source("app/pages/signup.wrn")).toContain("<SignUp");
expect(source("app/middleware/protected.ts")).toContain('requireAuth({ loginPath: "/login" })');
expect(source("app/pages/dashboard.wrn")).toContain("/api/auth/logout");
expect(source("app/pages/login.wrn")).toContain("AuthSplitLayout");
expect(source("app/pages/login.wrn")).toContain('data-slot="form"');
expect(source("app/pages/signup.wrn")).not.toContain("Already registered?");
expect(source("wrnexus.config.ts")).toContain('entry: "app/styles/global.css"');
});
test("authorization is database-backed and guards both APIs and administration", () => {
expect(source("app/middleware/authz.ts")).toContain("dbPermissionStore(getDb())");
expect(source("app/lib/auth.ts")).toContain('assignRole(user.id, "sales-rep")');
expect(source("app/api/contacts.ts")).toContain('can(ctx, "contact:write")');
expect(source("app/api/deals.ts")).toContain('can(ctx, "deal:read")');
expect(source("app/middleware/protected.ts")).toContain('can(ctx, "admin:access")');
});
test("the app includes public, authentication, and protected CRM pages", () => {
for (const page of [
"index",
"pricing",
"login",
"signup",
"dashboard",
"contacts",
"deals",
"admin",
"forbidden",
]) {
expect(source(`app/pages/${page}.wrn`)).toContain("page ");
}
});
test("protected CRM data is loaded on the server without browser API bindings", () => {
for (const page of ["contacts", "deals", "dashboard"]) {
const contents = source(`app/pages/${page}.wrn`);
expect(contents).toContain("load server");
expect(contents).not.toContain(' api="');
}
expect(source("app/api/dashboard.ts")).toContain("pipeline_value_cents");
expect(source("app/lib/workspace.ts")).toContain("INSERT OR IGNORE INTO crm_contacts");
expect(source("app/lib/auth.ts")).toContain("ensureCrmWorkspace(user.id)");
expect(source("app/middleware/protected.ts")).toContain('new URL("/forbidden", ctx.url)');
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
}
+38
View File
@@ -0,0 +1,38 @@
import { join } from "node:path";
import type { AuthConfig } from "@wrnexus/auth";
import type { AppConfig } from "@wrnexus/styles";
import { auth } from "./app/lib/auth.ts";
const config = {
seo: {
title: "Northstar CRM",
titleTemplate: "%s | Northstar CRM",
description: "A complete WrNexus SQLite CRM example.",
canonicalBase: "http://localhost:3000",
},
theme: { palette: "blue", default: "light" },
db: { driver: "sqlite", url: "file:./crm.sqlite" },
styles: {
entry: "app/styles/global.css",
async process({ entryPath, appRoot, mode }) {
const args = ["@tailwindcss/cli", "-i", entryPath!];
if (mode === "production") args.push("--minify");
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
},
failureMode: "throw",
},
auth: {
engine: auth,
routes: true,
middleware: true,
components: true,
migrations: true,
baseUrl: "http://localhost:3000",
},
profiles: {
test: { db: { driver: "sqlite", url: `file:${join(import.meta.dir, ".tmp-test.sqlite")}` } },
},
security: { cors: { enabled: false } },
} satisfies AppConfig & { auth: AuthConfig };
export default config;
+19 -7
View File
@@ -25,17 +25,17 @@ component SignIn {
class = "" class = ""
} }
view { view {
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'> <section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='wrn-auth-sign-in w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
<header class="mb-6 space-y-1.5"> <header class="wrn-auth-sign-in__header mb-6 space-y-1.5">
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1> <h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p> <p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
</header> </header>
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="space-y-4"> <form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="wrn-auth-sign-in__form">
<input type="hidden" name="returnTo" value='{returnTo}' /> <input type="hidden" name="returnTo" value='{returnTo}' />
<input type="hidden" name="deviceFingerprint" value="" /> <input type="hidden" name="deviceFingerprint" value="" />
<input type="hidden" name="deviceName" value="" /> <input type="hidden" name="deviceName" value="" />
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" /> <Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
<div class="space-y-1.5"> <div>
<TogglePassword <TogglePassword
label="{passwordLabel}" label="{passwordLabel}"
cornerHint="Forgot password?" cornerHint="Forgot password?"
@@ -48,9 +48,7 @@ component SignIn {
size="{size}" size="{size}"
/> />
</div> </div>
{#if showRemember} {#if showRemember}<div class="wrn-auth-sign-in__remember"><Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" /></div>{/if}
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
{/if}
<slot></slot> <slot></slot>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p> <p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /> <Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
@@ -64,4 +62,18 @@ component SignIn {
{/if} {/if}
</section> </section>
} }
style {
.wrn-auth-sign-in__header { margin-bottom: 1.5rem; }
.wrn-auth-sign-in__header h1 { font-size: 1.75rem; line-height: 1.15; }
.wrn-auth-sign-in__header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
.wrn-auth-sign-in__form { display: grid; gap: 1.15rem; }
.wrn-auth-sign-in__form > * { margin: 0 !important; }
.wrn-auth-sign-in__remember { margin-top: -0.25rem !important; }
.wrn-auth-sign-in__form .wrn-next__field-heading label,
.wrn-auth-sign-in__form .wrn-next__password-heading { font-size: 0.875rem; font-weight: 650; }
.wrn-auth-sign-in__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
.wrn-auth-sign-in__form [data-error]:empty { display: none; }
.wrn-auth-sign-in__form .wrn-action[data-full-width="true"] { margin-top: 0.15rem !important; }
}
} }
+13 -13
View File
@@ -27,7 +27,7 @@ component SignUp {
<section <section
{...attrs} {...attrs}
data-wrnexus-runtime="auth" data-wrnexus-runtime="auth"
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}' class='wrn-auth-sign-up w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
> >
<header <header
class="mb-6 space-y-1.5" class="mb-6 space-y-1.5"
@@ -50,7 +50,7 @@ component SignUp {
data-schema='{schema}' data-schema='{schema}'
data-redirect='{redirect}' data-redirect='{redirect}'
novalidate novalidate
class="grid gap-4 sm:grid-cols-2" class="wrn-auth-sign-up__form"
> >
<Input <Input
id="auth-sign-up-name" id="auth-sign-up-name"
@@ -61,7 +61,6 @@ component SignUp {
icon="icon-[lucide--user]" icon="icon-[lucide--user]"
color="{color}" color="{color}"
size="{size}" size="{size}"
class="sm:col-span-2"
/> />
<Input <Input
@@ -100,7 +99,6 @@ component SignUp {
icon="icon-[lucide--circle-user-round]" icon="icon-[lucide--circle-user-round]"
color="{color}" color="{color}"
size="{size}" size="{size}"
class="sm:col-span-2"
/> />
{/if} {/if}
@@ -176,15 +174,7 @@ component SignUp {
> >
</p> </p>
<Button <div class="wrn-auth-sign-up__submit"><Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /></div>
type="submit"
label="{submitLabel}"
variant="solid"
color="{color}"
size="{size}"
fullWidth="true"
class="wrn-auth-sign-up__wide sm:col-span-2"
/>
</form> </form>
<p <p
@@ -196,9 +186,19 @@ component SignUp {
} }
style { style {
.wrn-auth-sign-up > header { margin-bottom: 1.5rem; }
.wrn-auth-sign-up > header h1 { font-size: 1.75rem; line-height: 1.15; letter-spacing: -0.025em; }
.wrn-auth-sign-up > header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
.wrn-auth-sign-up__form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.15rem; }
.wrn-auth-sign-up__form .wrn-next__field-heading label,
.wrn-auth-sign-up__form .wrn-next--strong-password > label { font-size: 0.875rem; font-weight: 650; }
.wrn-auth-sign-up__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
.wrn-auth-sign-up__form [data-error]:empty { display: none; }
.wrn-auth-sign-up__wide { .wrn-auth-sign-up__wide {
grid-column: 1 / -1; grid-column: 1 / -1;
min-width: 0; min-width: 0;
} }
.wrn-auth-sign-up__submit { width: 100%; margin-top: 0.25rem; }
.wrn-auth-sign-up__submit .wrn-btn { min-height: 3rem; font-size: 0.9375rem; }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.12", "version": "0.8.13",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+11
View File
@@ -72,6 +72,17 @@ test("account forms use the dedicated password components", () => {
expect(signUp).not.toContain('id="auth-sign-up-password"'); expect(signUp).not.toContain('id="auth-sign-up-password"');
}); });
test("account forms own consistent spacing and full-width submit layout", () => {
const signIn = readFileSync(join(directory, "SignIn.wrn"), "utf8");
const signUp = readFileSync(join(directory, "SignUp.wrn"), "utf8");
expect(signIn).toContain("wrn-auth-sign-in__form");
expect(signIn).toContain("wrn-auth-sign-in__remember");
expect(signUp).toContain("wrn-auth-sign-up__form");
expect(signUp).toContain("wrn-auth-sign-up__submit");
expect(signUp).toContain("grid-template-columns: minmax(0, 1fr)");
});
test("authentication journeys use themed UI form controls", () => { test("authentication journeys use themed UI form controls", () => {
const expected = { const expected = {
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"], "SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.46", "version": "0.8.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+72 -5
View File
@@ -14,15 +14,23 @@
* (`databases.<name>`), with files under `app/db/<name>/`. * (`databases.<name>`), with files under `app/db/<name>/`.
*/ */
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path"; import { basename, join, resolve } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import {
createPluginRunner,
discoverPlugins,
type PackageMigrationDefinition,
} from "@wrnexus/plugin";
import { loadAppConfig, type AppConfig } from "@wrnexus/styles"; import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
import { import {
generateQueriesFile, generateQueriesFile,
analyzeMigrations, analyzeMigrations,
appliedMigrations,
applyMigrations,
loadMigrations, loadMigrations,
migrate, migrate,
parseMigration,
parseQueries, parseQueries,
rollback, rollback,
scaffoldMigration, scaffoldMigration,
@@ -42,6 +50,55 @@ function dbBaseOf(appDir: string, dbName: string | null): string {
return dbName ? join(appDir, "db", dbName) : join(appDir, "db"); return dbName ? join(appDir, "db", dbName) : join(appDir, "db");
} }
function packageMigrationName(definition: PackageMigrationDefinition, name?: string): string {
const clean = (value: string) => value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "_");
return name ? `${clean(definition.id)}__${clean(name)}` : clean(definition.id);
}
function resolvePackageMigrations(
definitions: readonly PackageMigrationDefinition[],
database?: string,
) {
return definitions.flatMap((definition) => {
if ((definition.database?.trim() || "default") !== (database?.trim() || "default")) return [];
if (definition.source !== undefined) {
return [parseMigration(packageMigrationName(definition), definition.source)];
}
if (!definition.entry || !existsSync(definition.entry)) {
throw new Error(`WRN-PLUGIN-MIGRATION-MISSING: ${definition.id} has no readable entry.`);
}
if (statSync(definition.entry).isDirectory()) {
return loadMigrations(definition.entry).map((migration) => ({
...migration,
name: packageMigrationName(definition, migration.name),
}));
}
return [
parseMigration(
packageMigrationName(definition, basename(definition.entry, ".sql")),
readFileSync(definition.entry, "utf8"),
),
];
});
}
async function packageMigrations(root: string, config: AppConfig, database?: string) {
const discovered = await discoverPlugins(root, config.plugins, {
includeDevDependencies: true,
strict: true,
});
const runner = createPluginRunner(discovered, {
root,
mode: "development",
command: "dev",
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
await runner.configure(config as Record<string, unknown>);
await runner.configResolved(config as Readonly<Record<string, unknown>>);
return resolvePackageMigrations((await runner.contributions()).migrations, database);
}
/** List user tables for the connected database (dialect-aware introspection). */ /** List user tables for the connected database (dialect-aware introspection). */
async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> { async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> {
const dialect = db.driver.dialect; const dialect = db.driver.dialect;
@@ -126,6 +183,7 @@ export async function runDbCommand(
const dbConfig = dbName ? config.databases?.[dbName] : config.db; const dbConfig = dbName ? config.databases?.[dbName] : config.db;
const dbBase = dbBaseOf(appDir, dbName); const dbBase = dbBaseOf(appDir, dbName);
const migrationsDir = join(dbBase, "migrations"); const migrationsDir = join(dbBase, "migrations");
const contributedMigrations = await packageMigrations(root, config, dbName ?? undefined);
const label = dbName ? ` (db: ${dbName})` : ""; const label = dbName ? ` (db: ${dbName})` : "";
const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir)); const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir));
@@ -242,7 +300,10 @@ export async function runDbCommand(
`WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`, `WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`,
); );
} }
const applied = await migrate(db, migrationsDir); const applied = [
...(await migrate(db, migrationsDir)),
...(await applyMigrations(db, contributedMigrations)),
];
console.log( console.log(
applied.length applied.length
? `✓ Applied ${applied.length}${label}:\n ${applied.join("\n ")}` ? `✓ Applied ${applied.length}${label}:\n ${applied.join("\n ")}`
@@ -257,8 +318,14 @@ export async function runDbCommand(
} }
case "status": { case "status": {
const rows = await status(db, migrationsDir); const rows = await status(db, migrationsDir);
if (rows.length === 0) console.log(`No migrations found in ${migrationsDir}.`); const applied = new Set(await appliedMigrations(db));
else for (const r of rows) console.log(` [${r.applied ? "x" : " "}] ${r.name}`); const packageRows = contributedMigrations.map(({ name }) => ({
name,
applied: applied.has(name),
}));
const allRows = [...rows, ...packageRows];
if (allRows.length === 0) console.log(`No migrations found in ${migrationsDir}.`);
else for (const r of allRows) console.log(` [${r.applied ? "x" : " "}] ${r.name}`);
break; break;
} }
default: default:
-1
View File
@@ -23,7 +23,6 @@ import {
mkdirSync, mkdirSync,
readFileSync, readFileSync,
readdirSync, readdirSync,
rmSync,
statSync, statSync,
writeFileSync, writeFileSync,
} from "node:fs"; } from "node:fs";
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.41", "version": "0.8.42",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+15 -1
View File
@@ -1482,13 +1482,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}); });
const apiCtx = createContext(apiReq, apiUrl); const apiCtx = createContext(apiReq, apiUrl);
apiCtx.locals = ctx.locals; apiCtx.locals = ctx.locals;
apiCtx.user = ctx.user;
apiCtx.tenant = ctx.tenant;
apiCtx.tracer = ctx.tracer;
apiCtx.ip = ctx.ip;
apiCtx.lang = ctx.lang;
apiCtx.t = ctx.t;
apiCtx.params = ctx.params;
apiCtx.cookies = ctx.cookies; apiCtx.cookies = ctx.cookies;
apiCtx.session = ctx.session; apiCtx.session = ctx.session;
apiCtx.localStorage = ctx.localStorage; apiCtx.localStorage = ctx.localStorage;
const apiRes = await handleApi(apiCtx); const apiRes = await handleApi(apiCtx);
if (!apiRes.ok) { if (!apiRes.ok) {
throw new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`); const contentType = apiRes.headers.get("content-type") ?? "";
const data = contentType.includes("application/json")
? await apiRes.json().catch(() => undefined)
: await apiRes.text().catch(() => undefined);
throw Object.assign(new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`), {
status: apiRes.status,
data,
});
} }
const contentType = apiRes.headers.get("content-type") ?? ""; const contentType = apiRes.headers.get("content-type") ?? "";
@@ -0,0 +1,98 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildRouter } from "@wrnexus/router";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
test("in-process api failures preserve status and response data for server error sections", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-error-context-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
mkdirSync(join(app, "api"), { recursive: true });
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
writeFileSync(join(app, "api", "failure.ts"), "export const GET = () => null;\n");
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
loadModule: async (file) =>
file.endsWith(`${join("api", "failure")}.ts`)
? {
GET: () => Response.json({ error: "denied", reason: "policy" }, { status: 403 }),
}
: {
__wrnexusLoad: async (ctx: {
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
}) => {
try {
await ctx.__wrnexusCallApi("/api/failure", "GET");
return { caught: null };
} catch (error) {
return {
caught: {
status: (error as { status?: unknown }).status,
data: (error as { data?: unknown }).data,
},
};
}
},
default: (ctx: { caught: unknown }) => `<pre>${JSON.stringify(ctx.caught)}</pre>`,
},
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/probe"), {
upgrade: () => false,
});
expect(response?.status).toBe(200);
expect(await response?.text()).toContain(
JSON.stringify({ status: 403, data: { error: "denied", reason: "policy" } }),
);
});
test("in-process api calls inherit authenticated request context", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-auth-context-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
mkdirSync(join(app, "api"), { recursive: true });
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
writeFileSync(join(app, "api", "identity.ts"), "export const GET = () => null;\n");
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
loadModule: async (file) =>
file.endsWith(`${join("api", "identity")}.ts`)
? { GET: (ctx: { user?: unknown }) => Response.json({ user: ctx.user }) }
: {
__wrnexusLoad: async (ctx: {
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
}) => ({ identity: await ctx.__wrnexusCallApi("/api/identity", "GET") }),
default: (ctx: { identity: unknown }) => `<pre>${JSON.stringify(ctx.identity)}</pre>`,
},
getMiddleware: async () => [
(ctx, next) => {
ctx.user = { id: "authenticated-user" };
return next();
},
],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/probe"), {
upgrade: () => false,
});
expect(response?.status).toBe(200);
expect(await response?.text()).toContain(JSON.stringify({ user: { id: "authenticated-user" } }));
});
+75 -3
View File
@@ -32,11 +32,14 @@ component StrongPassword {
required: boolean = false required: boolean = false
invalid: boolean = false invalid: boolean = false
validationMessage: string = "" validationMessage: string = ""
showLabel: string = "Show password"
hideLabel: string = "Hide password"
class: string = "" class: string = ""
} }
state password = value state password = value
state detailsOpen: boolean = false state detailsOpen: boolean = false
state revealed: boolean = false
functions { functions {
shared function hasMinimumLength() { shared function hasMinimumLength() {
@@ -113,6 +116,12 @@ component StrongPassword {
return emptyLabel return emptyLabel
} }
shared function toggleVisibility() {
if (!disabled && !readonly) {
revealed = !revealed
}
}
} }
view { view {
@@ -124,10 +133,11 @@ component StrongPassword {
> >
<label for="{name}-strong-password">{label}</label> <label for="{name}-strong-password">{label}</label>
<div class="wrn-next__strong-password-anchor"> <div class="wrn-next__strong-password-anchor">
<span class="wrn-next__strong-password-leading-icon" aria-hidden="true"></span>
<input <input
{...attrs} {...attrs}
id="{name}-strong-password" id="{name}-strong-password"
type="password" type="{revealed ? 'text' : 'password'}"
name="{name}" name="{name}"
value="{password}" value="{password}"
placeholder="{placeholder}" placeholder="{placeholder}"
@@ -143,6 +153,15 @@ component StrongPassword {
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })" @input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })" @change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
/> />
<button
type="button"
class="wrn-next__strong-password-toggle"
aria-label="{revealed ? hideLabel : showLabel}"
title="{revealed ? hideLabel : showLabel}"
aria-pressed="{revealed ? 'true' : 'false'}"
disabled="{disabled || readonly}"
@click="toggleVisibility()"
><span aria-hidden="true"></span></button>
{#if presentation === "popover"} {#if presentation === "popover"}
<div <div
@@ -236,19 +255,72 @@ component StrongPassword {
.wrn-next--strong-password input { .wrn-next--strong-password input {
width: 100%; width: 100%;
min-height: 2.65rem; min-height: 3rem;
padding: 0.7rem 0.85rem; padding: 0.75rem 3rem 0.75rem 2.75rem;
border: 1px solid var(--wrn-color-border); border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm); border-radius: var(--wrn-radius-sm);
outline: 0; outline: 0;
color: var(--wrn-color-text); color: var(--wrn-color-text);
background: var(--wrn-color-surface); background: var(--wrn-color-surface);
font: inherit; font: inherit;
font-size: 0.875rem;
transition: transition:
border-color 160ms ease, border-color 160ms ease,
box-shadow 160ms ease; box-shadow 160ms ease;
} }
.wrn-next__strong-password-leading-icon,
.wrn-next__strong-password-toggle {
position: absolute;
top: 50%;
z-index: 2;
transform: translateY(-50%);
}
.wrn-next__strong-password-leading-icon {
left: 0.9rem;
width: 1rem;
height: 1rem;
color: var(--wrn-color-muted);
background: currentColor;
pointer-events: none;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='11' x='3' y='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E") center / contain no-repeat;
}
.wrn-next__strong-password-toggle {
right: 0.45rem;
display: grid;
width: 2rem;
height: 2rem;
padding: 0;
place-items: center;
border: 0;
border-radius: calc(var(--wrn-radius-sm) * 0.72);
color: var(--wrn-color-muted);
background: transparent;
cursor: pointer;
}
.wrn-next__strong-password-toggle:hover,
.wrn-next__strong-password-toggle:focus-visible {
color: var(--wrn-component-color);
background: color-mix(in srgb, var(--wrn-component-color) 10%, transparent);
outline: 0;
}
.wrn-next__strong-password-toggle > span {
width: 1.05rem;
height: 1.05rem;
background: currentColor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.06 12.35a1 1 0 0 1 0-.7C3.7 7.6 7.64 5 12 5c4.36 0 8.3 2.6 9.94 6.65a1 1 0 0 1 0 .7C20.3 16.4 16.36 19 12 19c-4.36 0-8.3-2.6-9.94-6.65Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E") center / contain no-repeat;
}
.wrn-next__strong-password-toggle[aria-pressed="true"]::after {
position: absolute;
width: 1.25rem;
height: 0.12rem;
border-radius: 999px;
background: currentColor;
content: "";
transform: rotate(45deg);
box-shadow: 0 0 0 1px var(--wrn-color-surface);
}
.wrn-next--strong-password input:focus-visible { .wrn-next--strong-password input:focus-visible {
border-color: var(--wrn-component-color); border-color: var(--wrn-component-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 16%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 16%, transparent);
+17 -3
View File
@@ -107,6 +107,7 @@ component TogglePassword {
{/if} {/if}
</div> </div>
<div class="wrn-next__password-control"> <div class="wrn-next__password-control">
<span class="wrn-next__password-leading-icon" aria-hidden="true"></span>
<input <input
{...attrs} {...attrs}
id="{name}-password" id="{name}-password"
@@ -229,19 +230,32 @@ component TogglePassword {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
min-height: 2.75em; min-height: 2.75em;
padding: 0.65em 3em 0.65em 0.8em; min-height: 3rem;
padding: 0.75rem 3rem 0.75rem 2.75rem;
border: 1px solid var(--wrn-color-border); border: 1px solid var(--wrn-color-border);
border-radius: var(--wrn-radius-sm); border-radius: var(--wrn-radius-sm);
outline: 0; outline: 0;
color: var(--wrn-color-text); color: var(--wrn-color-text);
background: var(--wrn-color-bg); background: var(--wrn-color-surface);
font: inherit; font: inherit;
font-size: 0.8125rem; font-size: 0.875rem;
transition: transition:
border-color var(--wrn-motion-base) var(--wrn-ease-standard), border-color var(--wrn-motion-base) var(--wrn-ease-standard),
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard); box-shadow var(--wrn-motion-base) var(--wrn-ease-standard);
} }
.wrn-next__password-leading-icon {
position: absolute;
left: 0.9rem;
z-index: 1;
width: 1rem;
height: 1rem;
color: var(--wrn-color-muted);
background: currentColor;
pointer-events: none;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='11' x='3' y='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E") center / contain no-repeat;
}
.wrn-next__password-control > input:focus-visible { .wrn-next__password-control > input:focus-visible {
border-color: var(--wrn-component-color); border-color: var(--wrn-component-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent);
+6
View File
@@ -36,6 +36,7 @@ label: string = "Button"
view { view {
<span <span
class="wrn-action {fullWidth ? 'w-full' : ''} {class}" class="wrn-action {fullWidth ? 'w-full' : ''} {class}"
data-full-width="{fullWidth ? 'true' : 'false'}"
> >
{#if as === "a" || (as === "" && href)} {#if as === "a" || (as === "" && href)}
<a <a
@@ -414,6 +415,11 @@ label: string = "Button"
display: inline-flex; display: inline-flex;
} }
.wrn-action[data-full-width="true"],
.wrn-action[data-full-width="true"] > .wrn-btn {
width: 100%;
}
.wrn-btn--pill { .wrn-btn--pill {
border-radius: 999px; border-radius: 999px;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.20", "version": "0.8.21",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+12
View File
@@ -263,6 +263,18 @@ test("auth form uses package schemas, disables native validation, and renders a
expect(html).toContain('fullWidth="true"'); expect(html).toContain('fullWidth="true"');
}); });
test("password controls provide matching lock and visibility affordances", () => {
const toggle = readFileSync(uiComponentPath("TogglePassword"), "utf8");
const strong = readFileSync(uiComponentPath("StrongPassword"), "utf8");
const button = readFileSync(uiComponentPath("Button"), "utf8");
expect(toggle).toContain("wrn-next__password-leading-icon");
expect(strong).toContain("wrn-next__strong-password-leading-icon");
expect(strong).toContain("wrn-next__strong-password-toggle");
expect(strong).toContain("type=\"{revealed ? 'text' : 'password'}\"");
expect(button).toContain("data-full-width=\"{fullWidth ? 'true' : 'false'}\"");
});
test("footer supports typed entries, responsive columns, pre/post slots, and events", () => { test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
const source = readFileSync(uiComponentPath("Footer"), "utf8"); const source = readFileSync(uiComponentPath("Footer"), "utf8");
expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/); expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/);