feat(frontend): scaffold SvelteKit with TS 7.0 (native-preview) and tsgo

This commit is contained in:
2026-04-28 05:16:19 +02:00
parent 943463fff4
commit f9c721d841
2014 changed files with 415452 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
/** @import { BaseNode, Command, Visitors } from './types' */
export const margin = 0;
export const newline = 1;
export const indent = 2;
export const dedent = 3;
export const space = 4;
export class Context {
#visitors;
#commands;
#has_newline = false;
multiline = false;
/**
*
* @param {Visitors} visitors
* @param {Command[]} commands
*/
constructor(visitors, commands = []) {
this.#visitors = visitors;
this.#commands = commands;
}
indent() {
this.#commands.push(indent);
}
dedent() {
this.#commands.push(dedent);
}
margin() {
this.#commands.push(margin);
}
newline() {
this.#has_newline = true;
this.#commands.push(newline);
}
space() {
this.#commands.push(space);
}
/**
* @param {Context} context
*/
append(context) {
this.#commands.push(context.#commands);
if (this.#has_newline || context.multiline) {
this.multiline = true;
}
}
/**
*
* @param {string} content
* @param {BaseNode} [node]
*/
write(content, node) {
if (node?.loc) {
this.location(node.loc.start.line, node.loc.start.column);
this.#commands.push(content);
this.location(node.loc.end.line, node.loc.end.column);
} else {
this.#commands.push(content);
}
if (this.#has_newline) {
this.multiline = true;
}
}
/**
*
* @param {number} line
* @param {number} column
*/
location(line, column) {
this.#commands.push({ type: 'Location', line, column });
}
/**
* @param {{ type: string }} node
*/
visit(node) {
const visitor = this.#visitors[node.type];
if (!visitor) {
let message = `Not implemented: ${node.type}`;
if (node.type.includes('TS')) {
message += ` (consider using 'esrap/languages/ts')`;
}
if (node.type.includes('JSX')) {
message += ` (consider using 'esrap/languages/tsx')`;
}
throw new Error(message);
}
if (this.#visitors._) {
// @ts-ignore
this.#visitors._(node, this, (node) => visitor(node, this));
} else {
// @ts-ignore
visitor(node, this);
}
}
empty() {
return !this.#commands.some(has_content);
}
measure() {
return measure(this.#commands);
}
new() {
return new Context(this.#visitors);
}
}
/**
*
* @param {Command[]} commands
* @param {number} [from]
* @param {number} [to]
*/
function measure(commands, from = 0, to = commands.length) {
let total = 0;
for (let i = from; i < to; i += 1) {
const command = commands[i];
if (typeof command === 'string') {
total += command.length;
} else if (Array.isArray(command)) {
total += measure(command);
}
}
return total;
}
/**
* @param {Command} command
*/
function has_content(command) {
if (Array.isArray(command)) {
return command.some(has_content);
}
if (typeof command === 'string') {
return command.length > 0;
}
return false;
}
+165
View File
@@ -0,0 +1,165 @@
/** @import { BaseNode, Command, Visitors, PrintOptions } from './types' */
import { encode } from '@jridgewell/sourcemap-codec';
import { Context, dedent, indent, margin, newline, space } from './context.js';
/** @type {(str: string) => string} str */
let btoa = () => {
throw new Error('Unsupported environment: `window.btoa` or `Buffer` should be supported.');
};
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
btoa = (str) => window.btoa(unescape(encodeURIComponent(str)));
} else if (typeof Buffer === 'function') {
btoa = (str) => Buffer.from(str, 'utf-8').toString('base64');
}
class SourceMap {
version = 3;
/** @type {string[]} */
names = [];
/**
* @param {[number, number, number, number][][]} mappings
* @param {PrintOptions} opts
*/
constructor(mappings, opts) {
this.sources = [opts.sourceMapSource || null];
this.sourcesContent = [opts.sourceMapContent || null];
this.mappings = opts.sourceMapEncodeMappings === false ? mappings : encode(mappings);
}
/**
* Returns a JSON representation suitable for saving as a `*.map` file
*/
toString() {
return JSON.stringify(this);
}
/**
* Returns a base64-encoded JSON representation suitable for inlining at the bottom of a file with `//# sourceMappingURL={...}`
*/
toUrl() {
return 'data:application/json;charset=utf-8;base64,' + btoa(this.toString());
}
}
/**
* @template {BaseNode} [T=BaseNode]
* @param {T} node
* @param {Visitors<T>} visitors
* @param {PrintOptions} opts
* @returns {{ code: string, map: any }} // TODO
*/
export function print(node, visitors, opts = {}) {
/** @type {Command[]} */
const commands = [];
// @ts-expect-error some nonsense I don't understand
const context = new Context(visitors, commands);
context.visit(node);
/** @typedef {[number, number, number, number]} Segment */
let code = '';
let current_column = 0;
/** @type {Segment[][]} */
let mappings = [];
/** @type {Segment[]} */
let current_line = [];
/** @param {string} str */
function append(str) {
code += str;
for (let i = 0; i < str.length; i += 1) {
if (str[i] === '\n') {
mappings.push(current_line);
current_line = [];
current_column = 0;
} else {
current_column += 1;
}
}
}
let current_newline = '\n';
const indent_str = opts.indent ?? '\t';
let needs_newline = false;
let needs_margin = false;
let needs_space = false;
/** @param {Command} command */
function run(command) {
if (Array.isArray(command)) {
for (let i = 0; i < command.length; i += 1) {
run(command[i]);
}
return;
}
if (typeof command === 'number') {
if (command === newline) {
needs_newline = true;
} else if (command === margin) {
needs_margin = true;
} else if (command === space) {
needs_space = true;
} else if (command === indent) {
current_newline += indent_str;
} else if (command === dedent) {
current_newline = current_newline.slice(0, -indent_str.length);
}
return;
}
if (needs_newline) {
append(needs_margin ? '\n' + current_newline : current_newline);
} else if (needs_space) {
append(' ');
}
needs_margin = needs_newline = needs_space = false;
if (typeof command === 'string') {
append(command);
return;
}
if (command.type === 'Location') {
current_line.push([
current_column,
0, // source index is always zero
command.line - 1,
command.column
]);
}
}
for (let i = 0; i < commands.length; i += 1) {
run(commands[i]);
}
mappings.push(current_line);
/** @type {SourceMap} */
let map;
return {
code,
// create sourcemap lazily in case we don't need it
get map() {
return (map ??= new SourceMap(mappings, opts));
}
};
}
// it sucks that we have to export the class rather than just
// re-exporting it via public.d.ts, but otherwise TypeScript
// gets confused about private fields because it is really dumb!
export { Context };
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
import type { Visitors, BaseNode } from '../../types';
import type { TSOptions, BaseComment, Comment } from '../types';
export type { BaseComment, Comment };
export type Node = BaseNode;
export default function ts(options?: TSOptions): Visitors<BaseNode>;
// was Record<TSESTree.Expression['type'] | 'Super' | 'RestElement', number>
export declare const EXPRESSIONS_PRECEDENCE: Record<string, number>;
+137
View File
@@ -0,0 +1,137 @@
/** @import { TSESTree } from '@typescript-eslint/types' */
/** @import { Visitors } from '../../types.js' */
/** @import { TSOptions } from '../types.js' */
import ts from '../ts/index.js';
/**
* @param {TSOptions} [options]
* @returns {Visitors<TSESTree.Node>}
*/
export default (options) => ({
...ts(options),
JSXElement(node, context) {
context.visit(node.openingElement);
if (node.children.length > 0) {
context.indent();
}
for (const child of node.children) {
context.visit(child);
}
if (node.children.length > 0) {
context.dedent();
}
if (node.closingElement) {
context.visit(node.closingElement);
}
},
JSXOpeningElement(node, context) {
context.write('<');
context.visit(node.name);
for (const attribute of node.attributes) {
context.write(' ');
context.visit(attribute);
}
if (node.selfClosing) {
context.write(' /');
}
context.write('>');
},
JSXClosingElement(node, context) {
context.write('</');
context.visit(node.name);
context.write('>');
},
JSXNamespacedName(node, context) {
context.visit(node.namespace);
context.write(':');
context.visit(node.name);
},
JSXIdentifier(node, context) {
context.write(node.name, node);
},
JSXMemberExpression(node, context) {
context.visit(node.object);
context.write('.');
context.visit(node.property);
},
JSXText(node, context) {
context.write(node.value, node);
},
JSXAttribute(node, context) {
context.visit(node.name);
if (node.value) {
context.write('=');
context.visit(node.value);
}
},
JSXEmptyExpression(node, context) {},
JSXFragment(node, context) {
context.visit(node.openingFragment);
if (node.children.length > 0) {
context.indent();
}
for (const child of node.children) {
context.visit(child);
}
if (node.children.length > 0) {
context.dedent();
}
context.visit(node.closingFragment);
},
JSXOpeningFragment(node, context) {
context.write('<>');
},
JSXClosingFragment(node, context) {
context.write('</>');
},
JSXExpressionContainer(node, context) {
context.write('{');
context.visit(node.expression);
context.write('}');
},
JSXSpreadChild(node, context) {
context.write('{...');
context.visit(node.expression);
context.write('}');
},
JSXSpreadAttribute(node, context) {
context.write('{...');
context.visit(node.argument);
context.write('}');
}
});
+5
View File
@@ -0,0 +1,5 @@
import type { Visitors, BaseNode } from '../../types';
import type { TSOptions, BaseComment, Comment } from '../types';
export type { BaseComment, Comment };
export type Node = BaseNode;
export default function tsx(options?: TSOptions): Visitors<BaseNode>;
+29
View File
@@ -0,0 +1,29 @@
import type { BaseNode } from '../types';
export type TSOptions = {
quotes?: 'double' | 'single';
comments?: Comment[];
getLeadingComments?: (node: BaseNode) => BaseComment[] | undefined;
getTrailingComments?: (node: BaseNode) => BaseComment[] | undefined;
};
interface Position {
line: number;
column: number;
}
// this exists in TSESTree but because of the inanity around enums
// it's easier to do this ourselves
export interface BaseComment {
type: 'Line' | 'Block';
value: string;
start?: number;
end?: number;
}
export interface Comment extends BaseComment {
loc: {
start: Position;
end: Position;
};
}
+2
View File
@@ -0,0 +1,2 @@
export type { PrintOptions, Visitors } from './types';
export * from './index';
+48
View File
@@ -0,0 +1,48 @@
import type { Context } from 'esrap';
export type BaseNode = {
type: string;
loc?: null | {
start: { line: number; column: number };
end: { line: number; column: number };
};
};
type NodeOf<T extends string, X> = X extends { type: T } ? X : never;
type SpecialisedVisitors<T extends BaseNode> = {
[K in T['type']]?: Visitor<NodeOf<K, T>>;
};
export type Visitor<T> = (node: T, context: Context) => void;
export type Visitors<T extends BaseNode = BaseNode> = T['type'] extends '_'
? never
: SpecialisedVisitors<T> & { _?: (node: T, context: Context, visit: (node: T) => void) => void };
export { Context };
type TSExpressionWithTypeArguments = {
type: 'TSExpressionWithTypeArguments';
expression: any;
};
export interface Location {
type: 'Location';
line: number;
column: number;
}
export interface IndentChange {
type: 'IndentChange';
offset: number;
}
export type Command = string | number | Location | Command[];
export interface PrintOptions {
sourceMapSource?: string;
sourceMapContent?: string;
sourceMapEncodeMappings?: boolean; // default true
indent?: string; // default tab
}