feat: sign-in nip7

This commit is contained in:
Kieran 2024-09-23 13:28:45 +01:00
parent fb438c0dbc
commit eae46663d5
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
32 changed files with 596 additions and 361 deletions

View File

@ -10,7 +10,7 @@ steps:
- name: build - name: build
image: docker image: docker
privileged: true privileged: true
environment: environment:
TOKEN: TOKEN:
from_secret: registry_token from_secret: registry_token
commands: commands:
@ -18,4 +18,4 @@ steps:
- docker login -u registry -p $TOKEN registry.v0l.io - docker login -u registry -p $TOKEN registry.v0l.io
- docker build -t registry.v0l.io/lnvps-web:latest . - docker build -t registry.v0l.io/lnvps-web:latest .
- docker push registry.v0l.io/lnvps-web:latest - docker push registry.v0l.io/lnvps-web:latest
- kill $(cat /var/run/docker.pid) - kill $(cat /var/run/docker.pid)

View File

@ -1,6 +1,3 @@
{ {
"recommendations": [ "recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint"]
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint"
]
} }

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real eslint/bin/eslint.js your application uses // Defer to the real eslint/bin/eslint.js your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`)); module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real eslint your application uses // Defer to the real eslint your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint`)); module.exports = wrapWithUserWrapper(absRequire(`eslint`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real eslint/use-at-your-own-risk your application uses // Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`)); module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real typescript/bin/tsc your application uses // Defer to the real typescript/bin/tsc your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`)); module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real typescript/bin/tsserver your application uses // Defer to the real typescript/bin/tsserver your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`)); module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real typescript/lib/tsc.js your application uses // Defer to the real typescript/lib/tsc.js your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`)); module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`));

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,28 +25,30 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
const moduleWrapper = exports => { const moduleWrapper = (exports) => {
return wrapWithUserWrapper(moduleWrapperFn(exports)); return wrapWithUserWrapper(moduleWrapperFn(exports));
}; };
const moduleWrapperFn = tsserver => { const moduleWrapperFn = (tsserver) => {
if (!process.versions.pnp) { if (!process.versions.pnp) {
return tsserver; return tsserver;
} }
const {isAbsolute} = require(`path`); const { isAbsolute } = require(`path`);
const pnpApi = require(`pnpapi`); const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/"); const isPortal = (str) => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { const dependencyTreeRoots = new Set(
return `${locator.name}@${locator.reference}`; pnpApi.getDependencyTreeRoots().map((locator) => {
})); return `${locator.name}@${locator.reference}`;
}),
);
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS // VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol // doesn't understand. This layer makes sure to remove the protocol
@ -54,7 +56,11 @@ const moduleWrapperFn = tsserver => {
function toEditorPath(str) { function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths // We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { if (
isAbsolute(str) &&
!str.match(/^\^?(zip:|\/zip\/)/) &&
(str.match(/\.zip\//) || isVirtual(str))
) {
// We also take the opportunity to turn virtual paths into physical ones; // We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer // this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual // dependencies, since otherwise Ctrl+Click would bring us to the virtual
@ -68,7 +74,11 @@ const moduleWrapperFn = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) { if (resolved) {
const locator = pnpApi.findPackageLocator(resolved); const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { if (
locator &&
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
isPortal(locator.reference))
) {
str = resolved; str = resolved;
} }
} }
@ -96,41 +106,55 @@ const moduleWrapperFn = tsserver => {
// Before | ^/zip/c:/foo/bar.zip/package.json // Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json // After | ^/zip//c:/foo/bar.zip/package.json
// //
case `vscode <1.61`: { case `vscode <1.61`:
str = `^zip:${str}`; {
} break; str = `^zip:${str}`;
}
break;
case `vscode <1.66`: { case `vscode <1.66`:
str = `^/zip/${str}`; {
} break; str = `^/zip/${str}`;
}
break;
case `vscode <1.68`: { case `vscode <1.68`:
str = `^/zip${str}`; {
} break; str = `^/zip${str}`;
}
break;
case `vscode`: { case `vscode`:
str = `^/zip/${str}`; {
} break; str = `^/zip/${str}`;
}
break;
// To make "go to definition" work, // To make "go to definition" work,
// We have to resolve the actual file system path from virtual path // We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: { case `coc-nvim`:
str = normalize(resolved).replace(/\.zip\//, `.zip::`); {
str = resolve(`zipfile:${str}`); str = normalize(resolved).replace(/\.zip\//, `.zip::`);
} break; str = resolve(`zipfile:${str}`);
}
break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path, // We have to resolve the actual file system path from virtual path,
// everything else is up to neovim // everything else is up to neovim
case `neovim`: { case `neovim`:
str = normalize(resolved).replace(/\.zip\//, `.zip::`); {
str = `zipfile://${str}`; str = normalize(resolved).replace(/\.zip\//, `.zip::`);
} break; str = `zipfile://${str}`;
}
break;
default: { default:
str = `zip:${str}`; {
} break; str = `zip:${str}`;
}
break;
} }
} else { } else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
@ -142,26 +166,35 @@ const moduleWrapperFn = tsserver => {
function fromEditorPath(str) { function fromEditorPath(str) {
switch (hostInfo) { switch (hostInfo) {
case `coc-nvim`: { case `coc-nvim`:
str = str.replace(/\.zip::/, `.zip/`); {
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/... str = str.replace(/\.zip::/, `.zip/`);
// So in order to convert it back, we use .* to match all the thing // The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// before `zipfile:` // So in order to convert it back, we use .* to match all the thing
return process.platform === `win32` // before `zipfile:`
? str.replace(/^.*zipfile:\//, ``) return process.platform === `win32`
: str.replace(/^.*zipfile:/, ``); ? str.replace(/^.*zipfile:\//, ``)
} break; : str.replace(/^.*zipfile:/, ``);
}
break;
case `neovim`: { case `neovim`:
str = str.replace(/\.zip::/, `.zip/`); {
// The path for neovim is in format of zipfile:///<pwd>/.yarn/... str = str.replace(/\.zip::/, `.zip/`);
return str.replace(/^zipfile:\/\//, ``); // The path for neovim is in format of zipfile:///<pwd>/.yarn/...
} break; return str.replace(/^zipfile:\/\//, ``);
}
break;
case `vscode`: case `vscode`:
default: { default:
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) {
} break; return str.replace(
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
process.platform === `win32` ? `` : `/`,
);
}
break;
} }
} }
@ -173,8 +206,9 @@ const moduleWrapperFn = tsserver => {
// TypeScript already does local loads and if this code is running the user trusts the workspace // TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856 // https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject; const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
ConfiguredProject.prototype.enablePluginsWithOptions = function() { ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
this.projectService.allowLocalPluginLoads = true; this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments); return originalEnablePluginsWithOptions.apply(this, arguments);
}; };
@ -184,12 +218,13 @@ const moduleWrapperFn = tsserver => {
// like an absolute path of ours and normalize it. // like an absolute path of ours and normalize it.
const Session = tsserver.server.Session; const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; const { onMessage: originalOnMessage, send: originalSend } =
Session.prototype;
let hostInfo = `unknown`; let hostInfo = `unknown`;
Object.assign(Session.prototype, { Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) { onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string'; const isStringMessage = typeof message === "string";
const parsedMessage = isStringMessage ? JSON.parse(message) : message; const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if ( if (
@ -200,10 +235,12 @@ const moduleWrapperFn = tsserver => {
) { ) {
hostInfo = parsedMessage.arguments.hostInfo; hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( const [, major, minor] = (
// The RegExp from https://semver.org/ but without the caret at the start process.env.VSCODE_IPC_HOOK.match(
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ // The RegExp from https://semver.org/ but without the caret at the start
) ?? []).map(Number) /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
) ?? []
).map(Number);
if (major === 1) { if (major === 1) {
if (minor < 61) { if (minor < 61) {
@ -217,27 +254,39 @@ const moduleWrapperFn = tsserver => {
} }
} }
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { const processedMessageJSON = JSON.stringify(
return typeof value === 'string' ? fromEditorPath(value) : value; parsedMessage,
}); (key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
},
);
return originalOnMessage.call( return originalOnMessage.call(
this, this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) isStringMessage
? processedMessageJSON
: JSON.parse(processedMessageJSON),
); );
}, },
send(/** @type {any} */ msg) { send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { return originalSend.call(
return typeof value === `string` ? toEditorPath(value) : value; this,
}))); JSON.parse(
} JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
}),
),
);
},
}); });
return tsserver; return tsserver;
}; };
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); const [major, minor] = absRequire(`typescript/package.json`)
.version.split(`.`, 2)
.map((value) => parseInt(value, 10));
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. // In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
// Ref https://github.com/microsoft/TypeScript/pull/55326 // Ref https://github.com/microsoft/TypeScript/pull/55326
if (major > 5 || (major === 5 && minor >= 5)) { if (major > 5 || (major === 5 && minor >= 5)) {

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,28 +25,30 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
const moduleWrapper = exports => { const moduleWrapper = (exports) => {
return wrapWithUserWrapper(moduleWrapperFn(exports)); return wrapWithUserWrapper(moduleWrapperFn(exports));
}; };
const moduleWrapperFn = tsserver => { const moduleWrapperFn = (tsserver) => {
if (!process.versions.pnp) { if (!process.versions.pnp) {
return tsserver; return tsserver;
} }
const {isAbsolute} = require(`path`); const { isAbsolute } = require(`path`);
const pnpApi = require(`pnpapi`); const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/"); const isPortal = (str) => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { const dependencyTreeRoots = new Set(
return `${locator.name}@${locator.reference}`; pnpApi.getDependencyTreeRoots().map((locator) => {
})); return `${locator.name}@${locator.reference}`;
}),
);
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS // VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol // doesn't understand. This layer makes sure to remove the protocol
@ -54,7 +56,11 @@ const moduleWrapperFn = tsserver => {
function toEditorPath(str) { function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths // We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { if (
isAbsolute(str) &&
!str.match(/^\^?(zip:|\/zip\/)/) &&
(str.match(/\.zip\//) || isVirtual(str))
) {
// We also take the opportunity to turn virtual paths into physical ones; // We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer // this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual // dependencies, since otherwise Ctrl+Click would bring us to the virtual
@ -68,7 +74,11 @@ const moduleWrapperFn = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) { if (resolved) {
const locator = pnpApi.findPackageLocator(resolved); const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { if (
locator &&
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
isPortal(locator.reference))
) {
str = resolved; str = resolved;
} }
} }
@ -96,41 +106,55 @@ const moduleWrapperFn = tsserver => {
// Before | ^/zip/c:/foo/bar.zip/package.json // Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json // After | ^/zip//c:/foo/bar.zip/package.json
// //
case `vscode <1.61`: { case `vscode <1.61`:
str = `^zip:${str}`; {
} break; str = `^zip:${str}`;
}
break;
case `vscode <1.66`: { case `vscode <1.66`:
str = `^/zip/${str}`; {
} break; str = `^/zip/${str}`;
}
break;
case `vscode <1.68`: { case `vscode <1.68`:
str = `^/zip${str}`; {
} break; str = `^/zip${str}`;
}
break;
case `vscode`: { case `vscode`:
str = `^/zip/${str}`; {
} break; str = `^/zip/${str}`;
}
break;
// To make "go to definition" work, // To make "go to definition" work,
// We have to resolve the actual file system path from virtual path // We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: { case `coc-nvim`:
str = normalize(resolved).replace(/\.zip\//, `.zip::`); {
str = resolve(`zipfile:${str}`); str = normalize(resolved).replace(/\.zip\//, `.zip::`);
} break; str = resolve(`zipfile:${str}`);
}
break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path, // We have to resolve the actual file system path from virtual path,
// everything else is up to neovim // everything else is up to neovim
case `neovim`: { case `neovim`:
str = normalize(resolved).replace(/\.zip\//, `.zip::`); {
str = `zipfile://${str}`; str = normalize(resolved).replace(/\.zip\//, `.zip::`);
} break; str = `zipfile://${str}`;
}
break;
default: { default:
str = `zip:${str}`; {
} break; str = `zip:${str}`;
}
break;
} }
} else { } else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
@ -142,26 +166,35 @@ const moduleWrapperFn = tsserver => {
function fromEditorPath(str) { function fromEditorPath(str) {
switch (hostInfo) { switch (hostInfo) {
case `coc-nvim`: { case `coc-nvim`:
str = str.replace(/\.zip::/, `.zip/`); {
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/... str = str.replace(/\.zip::/, `.zip/`);
// So in order to convert it back, we use .* to match all the thing // The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// before `zipfile:` // So in order to convert it back, we use .* to match all the thing
return process.platform === `win32` // before `zipfile:`
? str.replace(/^.*zipfile:\//, ``) return process.platform === `win32`
: str.replace(/^.*zipfile:/, ``); ? str.replace(/^.*zipfile:\//, ``)
} break; : str.replace(/^.*zipfile:/, ``);
}
break;
case `neovim`: { case `neovim`:
str = str.replace(/\.zip::/, `.zip/`); {
// The path for neovim is in format of zipfile:///<pwd>/.yarn/... str = str.replace(/\.zip::/, `.zip/`);
return str.replace(/^zipfile:\/\//, ``); // The path for neovim is in format of zipfile:///<pwd>/.yarn/...
} break; return str.replace(/^zipfile:\/\//, ``);
}
break;
case `vscode`: case `vscode`:
default: { default:
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) {
} break; return str.replace(
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
process.platform === `win32` ? `` : `/`,
);
}
break;
} }
} }
@ -173,8 +206,9 @@ const moduleWrapperFn = tsserver => {
// TypeScript already does local loads and if this code is running the user trusts the workspace // TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856 // https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject; const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
ConfiguredProject.prototype.enablePluginsWithOptions = function() { ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
this.projectService.allowLocalPluginLoads = true; this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments); return originalEnablePluginsWithOptions.apply(this, arguments);
}; };
@ -184,12 +218,13 @@ const moduleWrapperFn = tsserver => {
// like an absolute path of ours and normalize it. // like an absolute path of ours and normalize it.
const Session = tsserver.server.Session; const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; const { onMessage: originalOnMessage, send: originalSend } =
Session.prototype;
let hostInfo = `unknown`; let hostInfo = `unknown`;
Object.assign(Session.prototype, { Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) { onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string'; const isStringMessage = typeof message === "string";
const parsedMessage = isStringMessage ? JSON.parse(message) : message; const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if ( if (
@ -200,10 +235,12 @@ const moduleWrapperFn = tsserver => {
) { ) {
hostInfo = parsedMessage.arguments.hostInfo; hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( const [, major, minor] = (
// The RegExp from https://semver.org/ but without the caret at the start process.env.VSCODE_IPC_HOOK.match(
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ // The RegExp from https://semver.org/ but without the caret at the start
) ?? []).map(Number) /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
) ?? []
).map(Number);
if (major === 1) { if (major === 1) {
if (minor < 61) { if (minor < 61) {
@ -217,27 +254,39 @@ const moduleWrapperFn = tsserver => {
} }
} }
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { const processedMessageJSON = JSON.stringify(
return typeof value === 'string' ? fromEditorPath(value) : value; parsedMessage,
}); (key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
},
);
return originalOnMessage.call( return originalOnMessage.call(
this, this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) isStringMessage
? processedMessageJSON
: JSON.parse(processedMessageJSON),
); );
}, },
send(/** @type {any} */ msg) { send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { return originalSend.call(
return typeof value === `string` ? toEditorPath(value) : value; this,
}))); JSON.parse(
} JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
}),
),
);
},
}); });
return tsserver; return tsserver;
}; };
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); const [major, minor] = absRequire(`typescript/package.json`)
.version.split(`.`, 2)
.map((value) => parseInt(value, 10));
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. // In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
// Ref https://github.com/microsoft/TypeScript/pull/55326 // Ref https://github.com/microsoft/TypeScript/pull/55326
if (major > 5 || (major === 5 && minor >= 5)) { if (major > 5 || (major === 5 && minor >= 5)) {

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const {existsSync} = require(`fs`); const { existsSync } = require(`fs`);
const {createRequire, register} = require(`module`); const { createRequire, register } = require(`module`);
const {resolve} = require(`path`); const { resolve } = require(`path`);
const {pathToFileURL} = require(`url`); const { pathToFileURL } = require(`url`);
const relPnpApiPath = "../../../../.pnp.cjs"; const relPnpApiPath = "../../../../.pnp.cjs";
@ -25,8 +25,8 @@ if (existsSync(absPnpApiPath)) {
} }
const wrapWithUserWrapper = existsSync(absUserWrapperPath) const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports) ? (exports) => absRequire(absUserWrapperPath)(exports)
: exports => exports; : (exports) => exports;
// Defer to the real typescript your application uses // Defer to the real typescript your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript`)); module.exports = wrapWithUserWrapper(absRequire(`typescript`));

View File

@ -18,11 +18,11 @@ export default tseslint.config({
languageOptions: { languageOptions: {
// other options... // other options...
parserOptions: { parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'], project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
}) });
``` ```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
@ -31,11 +31,11 @@ export default tseslint.config({
```js ```js
// eslint.config.js // eslint.config.js
import react from 'eslint-plugin-react' import react from "eslint-plugin-react";
export default tseslint.config({ export default tseslint.config({
// Set the react version // Set the react version
settings: { react: { version: '18.3' } }, settings: { react: { version: "18.3" } },
plugins: { plugins: {
// Add the react plugin // Add the react plugin
react, react,
@ -44,7 +44,7 @@ export default tseslint.config({
// other rules... // other rules...
// Enable its recommended rules // Enable its recommended rules
...react.configs.recommended.rules, ...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules, ...react.configs["jsx-runtime"].rules,
}, },
}) });
``` ```

View File

@ -1,26 +1,26 @@
import js from '@eslint/js' import js from "@eslint/js";
import globals from 'globals' import globals from "globals";
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from 'typescript-eslint' import tseslint from "typescript-eslint";
export default tseslint.config({ export default tseslint.config({
extends: [js.configs.recommended, ...tseslint.configs.recommended], extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
ignores: ['dist'], ignores: ["dist"],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
}, },
plugins: { plugins: {
'react-hooks': reactHooks, "react-hooks": reactHooks,
'react-refresh': reactRefresh, "react-refresh": reactRefresh,
}, },
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [ "react-refresh/only-export-components": [
'warn', "warn",
{ allowConstantExport: true }, { allowConstantExport: true },
], ],
}, },
}) });

View File

@ -1,31 +1,32 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Bitcoin Lightning VPS provider" />
<meta
name="keywords"
content="bitcoin lightning fast cheap vps virtual private server hosting web"
/>
<meta property="og:url" content="https://lnvps.net" />
<meta property="og:type" content="website" />
<meta property="og:title" content="LNVPS" />
<meta property="og:description" content="Bitcoin Lightning VPS provider" />
<meta property="og:image" content="/logo.png" />
<title>LNVPS</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
rel="stylesheet"
/>
</head>
<head> <body>
<meta charset="UTF-8" /> <div id="root"></div>
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <script type="module" src="/src/main.tsx"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://btcpay.v0l.io/modal/btcpay.js"></script>
<meta name="theme-color" content="#000000" /> </body>
<meta name="description" content="Bitcoin Lightning VPS provider" /> </html>
<meta
name="keywords"
content="bitcoin lightning fast cheap vps virtual private server hosting web" />
<meta property="og:url" content="https://lnvps.net" />
<meta property="og:type" content="website" />
<meta property="og:title" content="LNVPS" />
<meta property="og:description" content="Bitcoin Lightning VPS provider" />
<meta property="og:image" content="/logo.png" />
<title>LNVPS</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="https://btcpay.v0l.io/modal/btcpay.js"></script>
</body>
</html>

View File

@ -27,6 +27,7 @@
"eslint-plugin-react-refresh": "^0.4.9", "eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0", "globals": "^15.9.0",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.8", "tailwindcss": "^3.4.8",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -1,9 +1,12 @@
import { SnortContext } from "@snort/system-react" import { SnortContext } from "@snort/system-react";
import { CostInterval, DiskType, MachineSpec } from "./api" import { CostInterval, DiskType, MachineSpec } from "./api";
import VpsCard from "./components/vps-card" import VpsCard from "./components/vps-card";
import { GiB, NostrProfile } from "./const" import { GiB, NostrProfile } from "./const";
import { NostrSystem } from "@snort/system" import { NostrSystem } from "@snort/system";
import Profile from "./components/profile" import Profile from "./components/profile";
import { AsyncButton } from "./components/button";
import { loginNip7 } from "./login";
import LoginButton from "./components/login-button";
const Offers: Array<MachineSpec> = [ const Offers: Array<MachineSpec> = [
{ {
@ -13,13 +16,13 @@ const Offers: Array<MachineSpec> = [
ram: 2 * GiB, ram: 2 * GiB,
disk: { disk: {
type: DiskType.SSD, type: DiskType.SSD,
size: 80 * GiB size: 80 * GiB,
}, },
cost: { cost: {
interval: CostInterval.Month, interval: CostInterval.Month,
count: 3, count: 3,
currency: "EUR", currency: "EUR",
} },
}, },
{ {
id: "4x4x160", id: "4x4x160",
@ -28,13 +31,13 @@ const Offers: Array<MachineSpec> = [
ram: 4 * GiB, ram: 4 * GiB,
disk: { disk: {
type: DiskType.SSD, type: DiskType.SSD,
size: 160 * GiB size: 160 * GiB,
}, },
cost: { cost: {
interval: CostInterval.Month, interval: CostInterval.Month,
count: 5, count: 5,
currency: "EUR", currency: "EUR",
} },
}, },
{ {
id: "8x8x400", id: "8x8x400",
@ -43,50 +46,58 @@ const Offers: Array<MachineSpec> = [
ram: 8 * GiB, ram: 8 * GiB,
disk: { disk: {
type: DiskType.SSD, type: DiskType.SSD,
size: 400 * GiB size: 400 * GiB,
}, },
cost: { cost: {
interval: CostInterval.Month, interval: CostInterval.Month,
count: 12, count: 12,
currency: "EUR", currency: "EUR",
} },
} },
] ];
const system = new NostrSystem({}); const system = new NostrSystem({});
[ [
"wss://relay.snort.social/", "wss://relay.snort.social/",
"wss://relay.damus.io/", "wss://relay.damus.io/",
"wss://relay.nostr.band/", "wss://relay.nostr.band/",
"wss://nos.lol/" "wss://nos.lol/",
].forEach(a => system.ConnectToRelay(a, { read: true, write: true })); ].forEach((a) => system.ConnectToRelay(a, { read: true, write: true }));
export default function App() { export default function App() {
return ( return (
<SnortContext.Provider value={system}> <SnortContext.Provider value={system}>
<div className="w-[700px] mx-auto m-2 p-2"> <div className="w-[700px] mx-auto m-2 p-2">
<h1>LNVPS</h1> <div className="flex items-center justify-between">
LNVPS
<LoginButton />
</div>
<h1>VPS</h1> <h1>VPS Offers</h1>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{Offers.map(a => <VpsCard spec={a} />)} {Offers.map((a) => (
<VpsCard spec={a} />
))}
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<b>Please email <a href="mailto:sales@lnvps.net">sales</a> after paying the invoice with your order id, desired OS and ssh key.</b> <b>
Please email <a href="mailto:sales@lnvps.net">sales</a> after
paying the invoice with your order id, desired OS and ssh key.
</b>
<b>You can also find us on nostr: </b> <b>You can also find us on nostr: </b>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Profile link={NostrProfile} /> <Profile link={NostrProfile} />
<pre className="overflow-x-scroll">{NostrProfile.encode()}</pre> <pre className="overflow-x-scroll">{NostrProfile.encode()}</pre>
</div> </div>
<small> <small>
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic. All VPS come with 1x IPv4 and 1x IPv6 address and unmetered
traffic.
</small> </small>
</div> </div>
</div> </div>
</div> </div>
</SnortContext.Provider> </SnortContext.Provider>
) );
} }

21
src/components/button.tsx Normal file
View File

@ -0,0 +1,21 @@
import { forwardRef, HTMLProps } from "react";
export type AsyncButtonProps = {
onClick?: (e: React.MouseEvent) => Promise<void>;
} & Omit<HTMLProps<HTMLButtonElement>, "type" | "ref" | "onClick">;
const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
function AsyncButton(props, ref) {
return (
<button
ref={ref}
className="bg-slate-700 py-1 px-2 rounded-xl"
{...props}
>
{props.children}
</button>
);
},
);
export { AsyncButton };

View File

@ -1,19 +1,19 @@
import { GiB, KiB, MiB, TiB } from "../const" import { GiB, KiB, MiB, TiB } from "../const";
interface BytesSizeProps { interface BytesSizeProps {
value: number, value: number;
precision?: number precision?: number;
} }
export default function BytesSize(props: BytesSizeProps) { export default function BytesSize(props: BytesSizeProps) {
if (props.value >= TiB) { if (props.value >= TiB) {
return (props.value / TiB).toFixed(props.precision ?? 0) + "TB"; return (props.value / TiB).toFixed(props.precision ?? 0) + "TB";
} else if (props.value >= GiB) { } else if (props.value >= GiB) {
return (props.value / GiB).toFixed(props.precision ?? 0) + "GB"; return (props.value / GiB).toFixed(props.precision ?? 0) + "GB";
} else if (props.value >= MiB) { } else if (props.value >= MiB) {
return (props.value / MiB).toFixed(props.precision ?? 0) + "MB"; return (props.value / MiB).toFixed(props.precision ?? 0) + "MB";
} else if (props.value >= KiB) { } else if (props.value >= KiB) {
return (props.value / KiB).toFixed(props.precision ?? 0) + "KB"; return (props.value / KiB).toFixed(props.precision ?? 0) + "KB";
} else { } else {
return (props.value).toFixed(props.precision ?? 0) + "B"; return props.value.toFixed(props.precision ?? 0) + "B";
} }
} }

View File

@ -1,14 +1,22 @@
import { CostInterval, MachineSpec } from "../api"; import { CostInterval, MachineSpec } from "../api";
export default function CostLabel({ cost }: { cost: MachineSpec["cost"] }) { export default function CostLabel({ cost }: { cost: MachineSpec["cost"] }) {
function intervalName(n: number) { function intervalName(n: number) {
switch (n) { switch (n) {
case CostInterval.Hour: return "Hour" case CostInterval.Hour:
case CostInterval.Day: return "Day" return "Hour";
case CostInterval.Month: return "Month" case CostInterval.Day:
case CostInterval.Year: return "Year" return "Day";
} case CostInterval.Month:
return "Month";
case CostInterval.Year:
return "Year";
} }
}
return <>{cost.count} {cost.currency}/{intervalName(cost.interval)}</> return (
} <>
{cost.count} {cost.currency}/{intervalName(cost.interval)}
</>
);
}

View File

@ -0,0 +1,24 @@
import { SnortContext } from "@snort/system-react";
import { useContext } from "react";
import { AsyncButton } from "./button";
import { loginNip7 } from "../login";
import useLogin from "../hooks/login";
import Profile from "./profile";
import { NostrLink } from "@snort/system";
export default function LoginButton() {
const system = useContext(SnortContext);
const login = useLogin();
return !login ? (
<AsyncButton
onClick={async () => {
await loginNip7(system);
}}
>
Sign In
</AsyncButton>
) : (
<Profile link={NostrLink.publicKey(login.pubkey)} />
);
}

View File

@ -1,44 +1,64 @@
import { MachineSpec } from "../api"; import { MachineSpec } from "../api";
import "./pay-button.css" import "./pay-button.css";
declare global { declare global {
interface Window { interface Window {
btcpay?: { btcpay?: {
appendInvoiceFrame(invoiceId: string): void; appendInvoiceFrame(invoiceId: string): void;
} };
} }
} }
export default function VpsPayButton({ spec }: { spec: MachineSpec }) { export default function VpsPayButton({ spec }: { spec: MachineSpec }) {
const serverUrl = "https://btcpay.v0l.io/api/v1/invoices"; const serverUrl = "https://btcpay.v0l.io/api/v1/invoices";
function handleFormSubmit(event: React.FormEvent) { function handleFormSubmit(event: React.FormEvent) {
event.preventDefault(); event.preventDefault();
const form = event.target as HTMLFormElement; const form = event.target as HTMLFormElement;
const xhttp = new XMLHttpRequest(); const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () { xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200 && this.responseText) { if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay?.appendInvoiceFrame(JSON.parse(this.responseText).invoiceId); window.btcpay?.appendInvoiceFrame(
} JSON.parse(this.responseText).invoiceId,
}; );
xhttp.open('POST', serverUrl, true); }
xhttp.send(new FormData(form)); };
} xhttp.open("POST", serverUrl, true);
xhttp.send(new FormData(form));
}
if (!spec.active) { if (!spec.active) {
return <div className="text-center text-xl uppercase bg-red-800 rounded-xl py-3 font-bold"> return (
Unavailable <div className="text-center text-xl uppercase bg-red-800 rounded-xl py-3 font-bold">
</div> Unavailable
} </div>
);
}
return <form method="POST" action={serverUrl} className="btcpay-form btcpay-form--block" onSubmit={handleFormSubmit}> return (
<input type="hidden" name="storeId" value="CdaHy1puLx4kLC9BG3A9mu88XNyLJukMJRuuhAfbDrxg" /> <form
<input type="hidden" name="jsonResponse" value="true" /> method="POST"
<input type="hidden" name="orderId" value={spec.id} /> action={serverUrl}
<input type="hidden" name="price" value={spec.cost.count} /> className="btcpay-form btcpay-form--block w-full"
<input type="hidden" name="currency" value={spec.cost.currency} /> onSubmit={handleFormSubmit}
<input type="image" className="submit" name="submit" src="https://btcpay.v0l.io/img/paybutton/pay.svg" >
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor" /> <input
type="hidden"
name="storeId"
value="CdaHy1puLx4kLC9BG3A9mu88XNyLJukMJRuuhAfbDrxg"
/>
<input type="hidden" name="jsonResponse" value="true" />
<input type="hidden" name="orderId" value={spec.id} />
<input type="hidden" name="price" value={spec.cost.count} />
<input type="hidden" name="currency" value={spec.cost.currency} />
<input
type="image"
className="w-full"
name="submit"
src="https://btcpay.v0l.io/img/paybutton/pay.svg"
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"
/>
</form> </form>
} );
}

View File

@ -3,11 +3,18 @@ import { NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
export default function Profile({ link }: { link: NostrLink }) { export default function Profile({ link }: { link: NostrLink }) {
const profile = useUserProfile(link.id); const profile = useUserProfile(link.id);
return <div className="flex gap-2 items-center"> return (
<img src={profile?.picture} className="w-12 h-12 rounded-full bg-neutral-500" /> <div className="flex gap-2 items-center">
<div> <img
{profile?.display_name ?? profile?.name ?? hexToBech32("npub", link.id).slice(0, 12)} src={profile?.picture}
</div> className="w-12 h-12 rounded-full bg-neutral-500"
/>
<div>
{profile?.display_name ??
profile?.name ??
hexToBech32("npub", link.id).slice(0, 12)}
</div>
</div> </div>
} );
}

View File

@ -4,14 +4,23 @@ import CostLabel from "./cost";
import VpsPayButton from "./pay-button"; import VpsPayButton from "./pay-button";
export default function VpsCard({ spec }: { spec: MachineSpec }) { export default function VpsCard({ spec }: { spec: MachineSpec }) {
return <div className="rounded-xl border border-neutral-600 px-3 py-2"> return (
<h2>{spec.id}</h2> <div className="rounded-xl border border-neutral-600 px-3 py-2">
<ul> <h2>{spec.id}</h2>
<li>CPU: {spec.cpu}vCPU</li> <ul>
<li>RAM: <BytesSize value={spec.ram} /></li> <li>CPU: {spec.cpu}vCPU</li>
<li>{spec.disk.type === DiskType.SSD ? "SSD" : "HDD"}: <BytesSize value={spec.disk.size} /></li> <li>
</ul> RAM: <BytesSize value={spec.ram} />
<h2><CostLabel cost={spec.cost} /></h2> </li>
<VpsPayButton spec={spec} /> <li>
{spec.disk.type === DiskType.SSD ? "SSD" : "HDD"}:{" "}
<BytesSize value={spec.disk.size} />
</li>
</ul>
<h2>
<CostLabel cost={spec.cost} />
</h2>
<VpsPayButton spec={spec} />
</div> </div>
} );
}

View File

@ -12,9 +12,15 @@ export const GB = KB * 1000;
export const TB = GB * 1000; export const TB = GB * 1000;
export const PB = TB * 1000; export const PB = TB * 1000;
export const NostrProfile = new NostrLink(NostrPrefix.Profile, export const NostrProfile = new NostrLink(
"fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7", NostrPrefix.Profile,
undefined, undefined, [ "fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7",
"wss://nos.lol/", "wss://relay.nostr.bg/", "wss://relay.damus.io", "wss://relay.snort.social/" undefined,
] undefined,
[
"wss://nos.lol/",
"wss://relay.nostr.bg/",
"wss://relay.damus.io",
"wss://relay.snort.social/",
],
); );

12
src/hooks/login.tsx Normal file
View File

@ -0,0 +1,12 @@
import { useSyncExternalStore } from "react";
import { Login } from "../login";
export default function useLogin() {
return useSyncExternalStore(
(c) => {
Login?.on("change", c);
return () => Login?.off("change", c);
},
() => Login,
);
}

View File

@ -35,4 +35,4 @@ h3 {
a { a {
text-decoration: underline; text-decoration: underline;
} }

14
src/login.ts Normal file
View File

@ -0,0 +1,14 @@
import { Nip7Signer, SystemInterface, UserState } from "@snort/system";
export let Login: UserState<void> | undefined;
export async function loginNip7(system: SystemInterface) {
const signer = new Nip7Signer();
const pubkey = await signer.getPubKey();
if (pubkey) {
Login = new UserState<void>(pubkey);
await Login.init(signer, system);
} else {
throw new Error("No nostr extension found");
}
}

View File

@ -1,10 +1,10 @@
import { StrictMode } from 'react' import { StrictMode } from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import App from './App.tsx' import App from "./App.tsx";
import './index.css' import "./index.css";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode>, </StrictMode>,
) );

View File

@ -1,12 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} };

View File

@ -1,7 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) });

View File

@ -2548,6 +2548,7 @@ __metadata:
eslint-plugin-react-refresh: "npm:^0.4.9" eslint-plugin-react-refresh: "npm:^0.4.9"
globals: "npm:^15.9.0" globals: "npm:^15.9.0"
postcss: "npm:^8.4.41" postcss: "npm:^8.4.41"
prettier: "npm:^3.3.3"
react: "npm:^18.3.1" react: "npm:^18.3.1"
react-dom: "npm:^18.3.1" react-dom: "npm:^18.3.1"
tailwindcss: "npm:^3.4.8" tailwindcss: "npm:^3.4.8"
@ -3146,6 +3147,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^3.3.3":
version: 3.3.3
resolution: "prettier@npm:3.3.3"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26
languageName: node
linkType: hard
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": "proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "proc-log@npm:4.2.0" resolution: "proc-log@npm:4.2.0"