we typescript now

This commit is contained in:
Hamcha 2023-11-30 10:14:32 +01:00
parent a8d2ed380e
commit d7d814015f
14 changed files with 5147 additions and 106 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@
.env .env
.vscode .vscode
/result /result
/static/scripts/**/*.js*
/static/components/**/*.js*

12
.swcrc Normal file
View file

@ -0,0 +1,12 @@
{
"sourceMaps": true,
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": false,
"dynamicImport": false
},
"target": "esnext"
}
}

1846
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,3 +25,8 @@ time = { version = "0.3", features = ["serde"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
[build-dependencies]
swc = "0.269"
swc_common = { version = "0.33", features = ["tty-emitter"] }
glob = "0.3"

61
build.rs Normal file
View file

@ -0,0 +1,61 @@
use std::{
fs,
path::{Path, PathBuf},
};
use swc::config::{Options, SourceMapsConfig};
use swc_common::{
self,
errors::{ColorConfig, Handler},
sync::Lrc,
SourceMap, GLOBALS,
};
fn main() {
// Find all Typescript files (that are not type declaration)
let ts_files: Vec<_> = glob::glob(&format!("{}/**/*.ts", "static/source"))
.expect("Failed to read glob pattern")
.filter_map(|entry| entry.ok())
.filter(|entry| !entry.to_string_lossy().ends_with(".d.ts"))
.collect();
for ts_file in ts_files {
compile_file(ts_file);
}
}
fn compile_file(input: PathBuf) {
let cm: Lrc<SourceMap> = Default::default();
let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));
let c = swc::Compiler::new(cm.clone());
let fm = cm
.load_file(Path::new(&input))
.expect("failed to load input typescript file");
GLOBALS.set(&Default::default(), || {
let output_path = input.with_extension("js");
let sourcemap_path = input.with_extension("js.map");
let res = c
.process_js_file(
fm,
&handler,
&Options {
swcrc: true,
filename: input.to_string_lossy().into_owned(),
output_path: Some(output_path.clone()),
source_maps: Some(SourceMapsConfig::Bool(true)),
..Default::default()
},
)
.expect("parse error");
fs::create_dir_all(output_path.parent().unwrap()).expect("failed to create parent dir");
fs::write(output_path, res.code).expect("failed to write output file");
if let Some(map) = res.map {
fs::write(sourcemap_path, map).expect("failed to write output file");
}
});
}

View file

@ -1,47 +0,0 @@
/**
* Checks if a specificied source is a valid arion-compose.nix file
* @param {string} source Source to check
* @returns {Promise<CheckResult>} Result of the check
*/
export async function check_compose(source) {
const response = await fetch("/stack/_/check", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "text/plain",
},
body: source
});
if (response.ok) {
return { ok: true };
} else {
return { ok: false, error: await response.json() };
}
}
/**
* @typedef {Object} APIError
* @property {string} code - Error code
*/
/**
* @typedef {Object} CheckSuccess
* @property { true } ok - Indicates that the check was successful
* /
/**
* @typedef {Object} APIError
* @property {string} code - Unique error code
* @property {string} message - Human readable error details
*/
/**
* @typedef {Object} CheckFailure
* @property {false} ok - Indicates that the check has failed
* @property {APIError} error - Info about the encountered error
*/
/**
* @typedef {CheckSuccess | CheckFailure} CheckResult
*/

View file

@ -1,17 +0,0 @@
/**
* Find nearest parent that satisfies a requirement
* @param {Node|Element} node
* @param {(el: Element) => boolean} findFn
* @returns {HTMLElement | null} Found element, or null if no parent matches
*/
export function findNearestParent(node, findFn) {
let parent = node.parentElement;
while (parent) {
if (findFn(parent)) {
return parent;
}
parent = parent.parentElement;
}
return null;
}

View file

@ -1,24 +1,21 @@
import "/static/vendor/ace/ace.js"; import "/static/vendor/ace/ace.js";
import { $el } from "../../vendor/domutil/domutil.js"; import { $el } from "/static/vendor/domutil/domutil.js";
class Editor extends HTMLElement { class Editor extends HTMLElement {
editor; private editor: AceAjax.Editor;
private tabEl: HTMLDivElement;
/** @type {HTMLDivElement} Tab container */ private files: Record<string, AceAjax.IEditSession>;
tabEl; private root: ShadowRoot;
/** @type {Map<string, AceSession>} File name to file session */
files;
constructor() { constructor() {
super(); super();
this.files = {}; this.files = {};
this.attachShadow({ mode: "open" }); this.root = this.attachShadow({ mode: "open" });
} }
connectedCallback() { connectedCallback() {
this.tabEl = $el("div", { className: "tab-container" }); this.tabEl = $el("div", { className: "tab-container" });
this.shadowRoot.append(this.tabEl); this.root.append(this.tabEl);
const style = $el("style"); const style = $el("style");
style.textContent = ` style.textContent = `
@ -40,7 +37,7 @@ class Editor extends HTMLElement {
} }
`; `;
const slot = $el("slot", { name: "body" }); const slot = $el("slot", { name: "body" });
this.shadowRoot.append(style, slot); this.root.append(style, slot);
const editorEl = $el("div", { slot: "body" }); const editorEl = $el("div", { slot: "body" });
this.append(editorEl); this.append(editorEl);
@ -61,10 +58,10 @@ class Editor extends HTMLElement {
/** /**
* Add a file editing session * Add a file editing session
* @param {string} name File name * @param name File name
* @param {string} content File contents * @param content File contents
*/ */
addFile(name, content) { addFile(name: string, content: string) {
// Add to session list // Add to session list
this.files[name] = ace.createEditSession(content); this.files[name] = ace.createEditSession(content);
@ -78,10 +75,10 @@ class Editor extends HTMLElement {
/** /**
* Create a new tab * Create a new tab
* @param {string} name Tab name * @param name Tab name
* @returns Tab element * @returns Tab element
*/ */
#addTab(name) { #addTab(name: string) {
const tab = $el("div", { const tab = $el("div", {
className: "tab", className: "tab",
"@click": () => { "@click": () => {

36
static/scripts/compose.ts Normal file
View file

@ -0,0 +1,36 @@
/**
* Checks if a specificied source is a valid arion-compose.nix file
* @param source Source to check
* @returns Result of the check
*/
export async function check_compose(source: string): Promise<CheckResult> {
const response = await fetch("/stack/_/check", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "text/plain",
},
body: source
});
if (response.ok) {
return { ok: true };
} else {
return { ok: false, error: await response.json() };
}
}
export interface APIError {
/** Error code */
code: string;
/** Human readable error details */
message: string;
}
export type CheckResult = {
ok: true;
} | {
ok: false;
error: APIError;
}

View file

@ -1,12 +1,11 @@
import Editor from "../ace.mjs"; import { check_compose } from "/static/scripts/compose.js";
import { check_compose } from "/static/js/compose.mjs";
/** /**
* Makes the form require the stack to be checked before submitting * Makes the form require the stack to be checked before submitting
* @param {HTMLFormElement} form * @param form
* @param {Editor} editor * @param editor
*/ */
export function add_check(form, editor) { export function add_check(form: HTMLFormElement, editor: AceAjax.Editor) {
form.addEventListener("submit", (ev) => { form.addEventListener("submit", (ev) => {
ev.preventDefault(); ev.preventDefault();
check_stack(editor).then((result) => { check_stack(editor).then((result) => {
@ -20,15 +19,15 @@ export function add_check(form, editor) {
/** /**
* Runs the check function and updates some DOM elements * Runs the check function and updates some DOM elements
* @param {Editor} editor Editor instance * @param editor Editor instance
* @returns true if the stack is valid, false otherwise * @returns true if the stack is valid, false otherwise
*/ */
export async function check_stack(editor) { export async function check_stack(editor: AceAjax.Editor) {
const source = editor.editor.getValue(); const source = editor.getValue();
const check_result = await check_compose(source); const check_result = await check_compose(source);
const editorEl = document.querySelector(".ace_editor"); const editorEl = document.querySelector(".ace_editor")!;
const editorErrorEl = document.querySelector("#editor-form .error"); const editorErrorEl = document.querySelector<HTMLDivElement>("#editor-form div.error")!;
editorEl.classList.remove("err", "checked"); editorEl.classList.remove("err", "checked");
editorErrorEl.style.display = "block"; editorErrorEl.style.display = "block";
editorErrorEl.classList.add("pending"); editorErrorEl.classList.add("pending");

View file

@ -0,0 +1,16 @@
/**
* Find nearest parent that satisfies a requirement
* @param node Node to start from
* @param findFn Matching function
* @returns Found element, or null if no parent matches
*/
export function findNearestParent(node: Node | Element, findFn: (el: Element) => boolean): HTMLElement | null {
let parent = node.parentElement;
while (parent) {
if (findFn(parent)) {
return parent;
}
parent = parent.parentElement;
}
return null;
}

3139
static/vendor/ace/ace.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"paths": {
"/static/*": [
"./static/*"
],
},
"lib": [
"ESNext",
"DOM"
],
"rootDir": "./",
"baseUrl": "./",
"strictNullChecks": true,
"declaration": true,
},
"include": [
"./static/**/*.ts",
"./static/**/*.d.ts"
]
}