From e62d70607d4034a470cbcdec1d9c953265e9337f Mon Sep 17 00:00:00 2001 From: tamasfe Date: Sat, 18 Jun 2022 00:35:54 +0200 Subject: [PATCH] chore(js): add core to git --- js/core/.gitignore | 5 + js/core/.npmignore | 8 ++ js/core/README.md | 3 + js/core/package.json | 30 ++++++ js/core/rollup.config.js | 28 ++++++ js/core/src/config.ts | 127 ++++++++++++++++++++++++ js/core/src/environment.ts | 198 +++++++++++++++++++++++++++++++++++++ js/core/src/formatter.ts | 71 +++++++++++++ js/core/src/index.ts | 19 ++++ js/core/src/lsp.ts | 113 +++++++++++++++++++++ js/core/tsconfig.json | 14 +++ 11 files changed, 616 insertions(+) create mode 100644 js/core/.gitignore create mode 100644 js/core/.npmignore create mode 100644 js/core/README.md create mode 100644 js/core/package.json create mode 100644 js/core/rollup.config.js create mode 100644 js/core/src/config.ts create mode 100644 js/core/src/environment.ts create mode 100644 js/core/src/formatter.ts create mode 100644 js/core/src/index.ts create mode 100644 js/core/src/lsp.ts create mode 100644 js/core/tsconfig.json diff --git a/js/core/.gitignore b/js/core/.gitignore new file mode 100644 index 000000000..e081045ca --- /dev/null +++ b/js/core/.gitignore @@ -0,0 +1,5 @@ +dist +node_modules +target +**/.local* +*.tgz diff --git a/js/core/.npmignore b/js/core/.npmignore new file mode 100644 index 000000000..361644cad --- /dev/null +++ b/js/core/.npmignore @@ -0,0 +1,8 @@ +src +target +util +**/.local* +*.tgz +tsconfig.json +rollup.config.js +yarn-error.log diff --git a/js/core/README.md b/js/core/README.md new file mode 100644 index 000000000..6c14c5e8b --- /dev/null +++ b/js/core/README.md @@ -0,0 +1,3 @@ +Core utilities and types for Taplo, not intended for standalone use. + +All the documentation and information is available on the [website](https://taplo.tamasfe.dev). diff --git a/js/core/package.json b/js/core/package.json new file mode 100644 index 000000000..595122d9f --- /dev/null +++ b/js/core/package.json @@ -0,0 +1,30 @@ +{ + "name": "@taplo/core", + "version": "0.1.0", + "description": "Commonly used types for Taplo", + "author": { + "name": "tamasfe", + "url": "https://github.com/tamasfe" + }, + "scripts": { + "build": "yarn rollup --silent -c rollup.config.js", + "prepack": "yarn build" + }, + "types": "dist/index.d.ts", + "main": "dist/index.js", + "license": "MIT", + "autoTag": { + "enabled": true + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^22.0.0", + "@rollup/plugin-node-resolve": "^13.3.0", + "@types/node": "^17.0.41", + "esbuild": "^0.14.45", + "rollup": "^2.75.6", + "rollup-plugin-esbuild": "^4.9.1", + "tslib": "^2.4.0", + "typescript": "^4.7.3" + }, + "packageManager": "yarn@3.2.1" +} diff --git a/js/core/rollup.config.js b/js/core/rollup.config.js new file mode 100644 index 000000000..60a095bf7 --- /dev/null +++ b/js/core/rollup.config.js @@ -0,0 +1,28 @@ +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import path from "path"; +import process from "process"; +import { minify } from "rollup-plugin-esbuild"; +import typescript from "rollup-plugin-typescript2"; + +export default { + input: { + index: "src/index.ts", + }, + output: { + sourcemap: false, + name: "taploCore", + format: "umd", + dir: "dist", + }, + plugins: [ + typescript(), + commonjs(), + resolve({ + jsnext: true, + preferBuiltins: true, + rootDir: path.join(process.cwd(), ".."), + }), + minify(), + ], +}; diff --git a/js/core/src/config.ts b/js/core/src/config.ts new file mode 100644 index 000000000..43cbd9011 --- /dev/null +++ b/js/core/src/config.ts @@ -0,0 +1,127 @@ +import { FormatterOptions } from "./formatter"; + +export interface Config { + /** + * Files to exclude (ignore). + * + * A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns. Globstars (`**`) are supported. + * + * Relative paths are **not** relative to the configuration file, but rather depends on the tool using the configuration. + * + * This has priority over `include`. + */ + exclude?: string[]; + /** + * Formatting options. + */ + formatting?: FormatterOptions; + /** + * Files to include. + * + * A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns. Globstars (`**`) are supported. + * + * Relative paths are **not** relative to the configuration file, but rather depends on the tool using the configuration. + * + * Omitting this property includes all files, **however an empty array will include none**. + */ + include?: string[]; + /** + * Rules are used to override configurations by path and keys. + */ + rule?: Rule[]; + /** + * Schema validation options. + */ + schema?: SchemaOptions; +} + +/** + * A plugin to extend Taplo's capabilities. + */ +export interface Plugin { + /** + * Optional settings for the plugin. + */ + settings?: { + [k: string]: unknown; + }; + [k: string]: unknown; +} +/** + * A rule to override options by either name or file. + */ +export interface Rule { + /** + * Files that are excluded from this rule. + * + * A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns. + * + * Relative paths are **not** relative to the configuration file, but rather depends on the tool using the configuration. + * + * This has priority over `include`. + */ + exclude?: string[]; + /** + * Formatting options. + */ + formatting?: FormatterOptions; + /** + * Files this rule is valid for. + * + * A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns. + * + * Relative paths are **not** relative to the configuration file, but rather depends on the tool using the configuration. + * + * Omitting this property includes all files, **however an empty array will include none**. + */ + include?: string[]; + /** + * Keys the rule is valid for in a document. + * + * A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) dotted key patterns. + * + * This allows enabling the rule for specific paths in the document. + * + * For example: + * + * - `package.metadata` will enable the rule for everything inside the `package.metadata` table, including itself. + * + * If omitted, the rule will always be valid for all keys. + */ + keys?: string[]; + /** + * The name of the rule. + * + * Used in `taplo::` comments. + */ + name?: string; + /** + * Schema validation options. + */ + schema?: SchemaOptions; +} +/** + * Options for schema validation and completion. + * + * Schemas in rules with defined keys are ignored. + */ +export interface SchemaOptions { + /** + * Whether the schema should be enabled or not. + * + * Defaults to true if omitted. + */ + enabled?: boolean; + /** + * A local file path to the schema, overrides `url` if set. + * + * For URLs, please use `url` instead. + */ + path?: string; + /** + * A full absolute Url to the schema. + * + * The url of the schema, supported schemes are `http`, `https`, `file` and `taplo`. + */ + url?: string; +} diff --git a/js/core/src/environment.ts b/js/core/src/environment.ts new file mode 100644 index 000000000..550544edb --- /dev/null +++ b/js/core/src/environment.ts @@ -0,0 +1,198 @@ +import type { Readable, Writable } from "node:stream"; + +/** + * Environment required for several Taplo functions. + * + * This is required because WebAssembly is not self-contained and is sand-boxed. + */ +export interface Environment { + /** + * Return the current date. + */ + now: () => Date; + /** + * Return the environment variable, if any. + */ + envVar: (name: string) => string | undefined; + /** + * Return whether the standard error output is a tty or not. + */ + stdErrAtty: () => boolean; + /** + * Read `n` bytes from the standard input. + * + * If the returned array is empty, EOF is reached. + * + * This function must not return more than `n` bytes. + */ + stdin: Readable | ((n: bigint) => Promise); + /** + * Write the given bytes to the standard output returning + * the number of bytes written. + */ + stdout: Writable | ((bytes: Uint8Array) => Promise); + /** + * Write the given bytes to the standard error output returning + * the number of bytes written. + */ + stderr: Writable | ((bytes: Uint8Array) => Promise); + /** + * Search a glob file pattern and return the matched files. + */ + glob: (pattern: string) => Array; + /** + * Read the contents of the file at the given path. + */ + readFile: (path: string) => Promise; + /** + * Write and overwrite a file at the given path. + */ + writeFile: (path: string, bytes: Uint8Array) => Promise; + /** + * Turn an URL into a file path. + */ + urlToFilePath: (url: string) => string; + /** + * Return whether a path is absolute. + */ + isAbsolute: (path: string) => boolean; + /** + * Return the path to the current working directory. + */ + cwd: () => string; + /** + * Find the Taplo config file from the given directory + * and return the path if found. + * + * The following files should be searched in order from the given root: + * + * - `.taplo.toml` + * - `taplo.toml` + */ + findConfigFile: (from: string) => string | undefined; + /** + * The fetch function if it is not defined on the global Window. + * + * This is required for environments like NodeJs where the fetch API is not available, + * so a package like `node-fetch` must be used instead. + * + */ + fetch?: { + fetch: any; + Headers: any; + Request: any; + Response: any; + }; +} +/** + * @private + */ +export function prepareEnv(environment: Environment) { + if (typeof fetch === "undefined") { + if (environment.fetch) { + // FIXME: A lot of assumptions here... + (global as any).Headers = environment.fetch.Headers; + (global as any).Request = environment.fetch.Request; + (global as any).Response = environment.fetch.Response; + (global as any).fetch = environment.fetch.fetch; + } else { + console.warn( + "fetch was not provided, HTTP operations will not be possible" + ); + } + } +} + +/** + * @private + */ +export function convertEnv(env: Environment): any { + const stdin = + typeof env.stdin === "function" ? env.stdin : streamToReadCb(env.stdin); + const stdout = + typeof env.stdout === "function" ? env.stdout : streamToWriteCb(env.stdout); + const stderr = + typeof env.stderr === "function" ? env.stderr : streamToWriteCb(env.stderr); + + return { + js_now: env.now, + js_env_var: env.envVar, + js_atty_stderr: env.stdErrAtty, + js_on_stdin: stdin, + js_on_stdout: stdout, + js_on_stderr: stderr, + js_glob_files: env.glob, + js_read_file: env.readFile, + js_write_file: env.writeFile, + js_to_file_path: env.urlToFilePath, + js_is_absolute: env.isAbsolute, + js_cwd: env.cwd, + js_find_config_file: env.findConfigFile, + }; +} + +function streamToWriteCb( + stream: Writable +): (bytes: Uint8Array) => Promise { + return bytes => { + return new Promise(resolve => { + // FIXME: we immediately resolve as it does not matter + // in any of the use-cases. + stream.write(bytes); + resolve(bytes.length); + }); + }; +} + +function streamToReadCb(stream: Readable): (n: bigint) => Promise { + // The stream EOF event callback is immediately called after the last + // bit of data was read, however we cannot immediately signal it as we are still returning data. + // + // If EOF happens, subsequent stream events will not happen, not even "end" and the promise + // will get stuck and nodejs will terminate without any errors (found it out the hard way). + // + // So we keep track of EOF here and immediately return 0 bytes on the next call without + // touching the stream. + let eof = false; + + return n => { + // Make sure that we only resolve/reject the promise once. + // This might not be necessary, but it's better to be safe. + let done = false; + + return new Promise((resolve, reject) => { + if (eof) { + return resolve(new Uint8Array()); + } + + function onReadable() { + const data = stream.read(Number(n)); + if (data !== null) { + if (!done) { + done = true; + resolve(data); + stream.off("readable", onReadable); + } + } + } + + stream.on("readable", onReadable); + + stream.once("end", () => { + eof = true; + if (!done) { + done = true; + resolve(new Uint8Array()); + } + }); + + stream.once("error", err => { + if (!done) { + console.log("error"); + done = true; + reject(err); + } + }); + }); + }; +} diff --git a/js/core/src/formatter.ts b/js/core/src/formatter.ts new file mode 100644 index 000000000..5a3e3edc5 --- /dev/null +++ b/js/core/src/formatter.ts @@ -0,0 +1,71 @@ +/** + * Taplo formatter options. + */ +export interface FormatterOptions { + /** + * Align consecutive entries vertically. + */ + alignEntries?: boolean; + /** Align consecutive comments after entries and items vertically. + * + * This applies to comments that are after entries or array items. + */ + alignComments?: boolean; + /** + * Append trailing commas for multi-line arrays. + */ + arrayTrailingComma?: boolean; + /** + * Expand arrays to multiple lines that exceed the maximum column width. + */ + arrayAutoExpand?: boolean; + /** + * Collapse arrays that don't exceed the maximum column width and don't contain comments. + */ + arrayAutoCollapse?: boolean; + /** + * Omit white space padding from single-line arrays + */ + compactArrays?: boolean; + /** + * Omit white space padding from the start and end of inline tables. + */ + compactInlineTables?: boolean; + /** + * Omit white space around `=`. + */ + compactEntries?: boolean; + /** + * Maximum column width in characters, affects array expansion and collapse, this doesn't take whitespace into account. + * Note that this is not set in stone, and works on a best-effort basis. + */ + columnWidth?: number; + /** + * Indent based on tables and arrays of tables and their subtables, subtables out of order are not indented. + */ + indentTables?: boolean; + /** + * Indent entries under tables. + */ + indentEntries?: boolean; + /** + * The substring that is used for indentation, should be tabs or spaces (but technically can be anything). + */ + indentString?: string; + /** + * Add trailing newline at the end of the file if not present. + */ + trailingNewline?: boolean; + /** + * Alphabetically reorder keys that are not separated by empty lines. + */ + reorderKeys?: boolean; + /** + * Maximum amount of allowed consecutive blank lines. This does not affect the whitespace at the end of the document, as it is always stripped. + */ + allowedBlankLines?: number; + /** + * Use CRLF for line endings. + */ + crlf?: boolean; +} diff --git a/js/core/src/index.ts b/js/core/src/index.ts new file mode 100644 index 000000000..ed53d5e24 --- /dev/null +++ b/js/core/src/index.ts @@ -0,0 +1,19 @@ +export * from "./environment"; +export * from "./formatter"; +export * as Lsp from "./lsp"; +export * from "./config"; + +/** + * Byte range within a TOML document. + */ +export interface Range { + /** + * Start byte index. + */ + start: number; + /** + * Exclusive end index. + */ + end: number; +} + diff --git a/js/core/src/lsp.ts b/js/core/src/lsp.ts new file mode 100644 index 000000000..44bac2c96 --- /dev/null +++ b/js/core/src/lsp.ts @@ -0,0 +1,113 @@ +export namespace Server { + interface ServerNotifications { + "taplo/messageWithOutput": { + params: { + kind: "info" | "warn" | "error"; + message: string; + }; + }; + "taplo/didChangeSchemaAssociation": { + params: { + documentUri: string; + schemaUri?: string | null; + meta?: any; + }; + }; + } + + export type NotificationMethod = keyof ServerNotifications; + + export type NotificationParams = + ServerNotifications[T] extends NotificationDescription + ? ServerNotifications[T]["params"] + : never; +} + +export namespace Client { + interface ClientNotifications { + "taplo/associateSchema": { + params: { + document_uri?: string | null; + schema_uri: string; + rule: AssociationRule; + priority?: number | null; + meta?: any; + }; + }; + } + + interface ClientRequests { + "taplo/convertToJson": { + params: { + text: string; + }; + response: { + text?: string | null; + error?: string | null; + }; + }; + "taplo/convertToToml": { + params: { + text: string; + }; + response: { + text?: string | null; + error?: string | null; + }; + }; + "taplo/listSchemas": { + params: { + documentUri: string; + }; + response: { + schemas: Array; + }; + }; + "taplo/associatedSchema": { + params: { + documentUri: string; + }; + response: { + schema?: SchemaInfo | null; + }; + }; + } + + export type NotificationMethod = keyof ClientNotifications; + + export type NotificationParams = + ClientNotifications[T] extends NotificationDescription + ? ClientNotifications[T]["params"] + : never; + + export type RequestMethod = keyof ClientRequests; + + export type RequestParams = + ClientRequests[T] extends RequestDescription + ? ClientRequests[T]["params"] + : never; + + export type RequestResponse = + ClientRequests[T] extends RequestDescription + ? ClientRequests[T]["response"] + : never; +} + +interface NotificationDescription { + readonly params: any; +} + +interface RequestDescription { + readonly params: any; + readonly response: any; +} + +export type AssociationRule = + | { glob: string } + | { regex: string } + | { url: string }; + +export interface SchemaInfo { + url: string; + meta: any; +} diff --git a/js/core/tsconfig.json b/js/core/tsconfig.json new file mode 100644 index 000000000..53188fef9 --- /dev/null +++ b/js/core/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2019", + "lib": ["ES2019", "dom"], + "sourceMap": false, + "rootDir": "src", + "moduleResolution": "node", + "module": "ESNext", + "allowSyntheticDefaultImports": true, + "declaration": true, + "strict": false + }, + "exclude": ["node_modules"] +}