diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55f8a7c37143d..a9c528e0c0558 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,60 +94,6 @@ jobs: rustc --version cargo --version - - name: Configure cargo data directory - # After this point, all cargo registry and crate data is stored in - # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files - # that are needed during the build process. Additionally, this works - # around a bug in the 'cache' action that causes directories outside of - # the workspace dir to be saved/restored incorrectly. - run: echo "::set-env name=CARGO_HOME::$(pwd)/.cargo_home" - - - name: Cache - uses: actions/cache@v2 - with: - # Note: crates from the denoland/deno git repo always get rebuilt, - # and their outputs ('deno', 'libdeno.rlib' etc.) are quite big, - # so we cache only those subdirectories of target/{debug|release} that - # contain the build output for crates that come from the registry. - path: |- - .cargo_home - target/*/.* - target/*/build - target/*/deps - target/*/gn_out - key: deno-${{ matrix.os }}-${{ matrix.kind }}-${{ hashFiles('Cargo.lock') }} - restore-keys: | - deno-${{ matrix.os }}-${{ matrix.kind }}- - - # It seems that the 'target' directory does not always get restored - # from cache correctly on MacOS. In the build log we see the following: - # - # Fresh serde_derive v1.0.115 - # - # But a little while after that Cargo aborts because 'serde_derive' is - # now nowhere to be found. We're not the only ones experiencing this, - # see https://github.com/actions-rs/cargo/issues/111. - # - # error[E0463]: can't find crate for `serde_derive` - # ##[error] --> /Users/runner/.cargo/registry/src/github.com- - # | 1ecc6299db9ec823/serde-1.0.115/src/lib.rs:285:1 - # | - # 285 | extern crate serde_derive; - # | ^^^^^^^^^^^^^^^^^^^^^^^^^^ can't find crate - - name: Work around MacOS + Cargo + Github Actions cache bug - if: runner.os == 'macOS' - run: | - cargo clean --locked --release \ - -p ast_node \ - -p is-macro \ - -p serde_derive \ - -p swc_ecma_codegen \ - -p swc_ecma_codegen_macros \ - -p swc_ecma_parser \ - -p swc_ecma_parser_macros \ - -p swc_visit \ - -p swc_visit_macros - - name: lint.py if: matrix.kind == 'lint' run: python ./tools/lint.py diff --git a/Cargo.lock b/Cargo.lock index 1281c34b6c54a..39ddd5cf3d644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,7 +450,7 @@ dependencies = [ [[package]] name = "deno_core" -version = "0.63.0" +version = "0.64.0" dependencies = [ "anyhow", "futures", @@ -1919,9 +1919,9 @@ dependencies = [ [[package]] name = "rusty_v8" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c11463988ec37b3f8cb84e4c2fe8f63058c46e07348b6d1d27b114a2b981304" +checksum = "692dddfb8ae9915b19774a05cadc71363c98bad4fafc13636579da5a7a110016" dependencies = [ "bitflags", "cargo_gn", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index efe815dc553f8..28ed44a8ddd8f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,7 +20,7 @@ harness = false path = "./bench/main.rs" [build-dependencies] -deno_core = { path = "../core", version = "0.63.0" } +deno_core = { path = "../core", version = "0.64.0" } deno_web = { path = "../op_crates/web", version = "0.15.0" } deno_fetch = { path = "../op_crates/fetch", version = "0.7.0" } @@ -29,7 +29,7 @@ winres = "0.1.11" winapi = "0.3.9" [dependencies] -deno_core = { path = "../core", version = "0.63.0" } +deno_core = { path = "../core", version = "0.64.0" } deno_doc = "0.1.12" deno_lint = "0.2.4" deno_web = { path = "../op_crates/web", version = "0.15.0" } diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index 0b9876865e615..d33eb8a0b9c86 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -1226,7 +1226,7 @@ declare namespace Deno { export function rename(oldpath: string, newpath: string): Promise; /** Synchronously reads and returns the entire contents of a file as utf8 - * encoded string. Reading a directory returns an empty string. + * encoded string. Reading a directory throws an error. * * ```ts * const data = Deno.readTextFileSync("hello.txt"); @@ -1237,7 +1237,7 @@ declare namespace Deno { export function readTextFileSync(path: string | URL): string; /** Asynchronously reads and returns the entire contents of a file as utf8 - * encoded string. Reading a directory returns an empty string. + * encoded string. Reading a directory throws an error. * * ```ts * const data = await Deno.readTextFile("hello.txt"); diff --git a/cli/lint.rs b/cli/lint.rs index 882ec44145b30..a2a1252c025b8 100644 --- a/cli/lint.rs +++ b/cli/lint.rs @@ -222,6 +222,7 @@ impl LintReporter for PrettyLintReporter { &pretty_message, &source_lines, d.range.clone(), + d.hint.as_ref(), &fmt_errors::format_location(&JsStackFrame::from_location( Some(d.filename.clone()), Some(d.range.start.line as i64), @@ -256,6 +257,7 @@ pub fn format_diagnostic( message_line: &str, source_lines: &[&str], range: deno_lint::diagnostic::Range, + maybe_hint: Option<&String>, formatted_location: &str, ) -> String { let mut lines = vec![]; @@ -284,12 +286,23 @@ pub fn format_diagnostic( } } - format!( - "{}\n{}\n at {}", - message_line, - lines.join("\n"), - formatted_location - ) + if let Some(hint) = maybe_hint { + format!( + "{}\n{}\n at {}\n\n {} {}", + message_line, + lines.join("\n"), + formatted_location, + colors::gray("hint:"), + hint, + ) + } else { + format!( + "{}\n{}\n at {}", + message_line, + lines.join("\n"), + formatted_location + ) + } } #[derive(Serialize)] diff --git a/cli/repl.rs b/cli/repl.rs index 3540b422b2997..3d22f8156ea59 100644 --- a/cli/repl.rs +++ b/cli/repl.rs @@ -12,7 +12,6 @@ use regex::Captures; use regex::Regex; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; -use rustyline::validate::MatchingBracketValidator; use rustyline::validate::ValidationContext; use rustyline::validate::ValidationResult; use rustyline::validate::Validator; @@ -26,7 +25,6 @@ use std::sync::Mutex; #[derive(Completer, Helper, Hinter)] struct Helper { highlighter: LineHighlighter, - validator: MatchingBracketValidator, } impl Validator for Helper { @@ -34,7 +32,42 @@ impl Validator for Helper { &self, ctx: &mut ValidationContext, ) -> Result { - self.validator.validate(ctx) + let mut stack: Vec = Vec::new(); + for c in ctx.input().chars() { + match c { + '(' | '[' | '{' => stack.push(c), + ')' | ']' | '}' => match (stack.pop(), c) { + (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} + (Some(left), _) => { + return Ok(ValidationResult::Invalid(Some(format!( + "Mismatched pairs: {:?} is not properly closed", + left + )))) + } + (None, c) => { + return Ok(ValidationResult::Invalid(Some(format!( + "Mismatched pairs: {:?} is unpaired", + c + )))) + } + }, + '`' => { + if stack.is_empty() || stack.last().unwrap() != &c { + stack.push(c); + } else { + stack.pop(); + } + } + + _ => {} + } + } + + if !stack.is_empty() { + return Ok(ValidationResult::Incomplete); + } + + Ok(ValidationResult::Valid(None)) } } @@ -217,6 +250,31 @@ async fn inject_prelude( Ok(()) } +pub async fn is_closing( + worker: &mut MainWorker, + session: &mut InspectorSession, + context_id: u64, +) -> Result { + let closed = post_message_and_poll( + worker, + session, + "Runtime.evaluate", + Some(json!({ + "expression": "(globalThis.closed)", + "contextId": context_id, + })), + ) + .await? + .get("result") + .unwrap() + .get("value") + .unwrap() + .as_bool() + .unwrap(); + + Ok(closed) +} + pub async fn run( program_state: &ProgramState, mut worker: MainWorker, @@ -249,7 +307,6 @@ pub async fn run( let helper = Helper { highlighter: LineHighlighter::new(), - validator: MatchingBracketValidator::new(), }; let editor = Arc::new(Mutex::new(Editor::new())); @@ -267,7 +324,7 @@ pub async fn run( inject_prelude(&mut worker, &mut session, context_id).await?; - loop { + while !is_closing(&mut worker, &mut session, context_id).await? { let line = read_line_and_poll(&mut *worker, editor.clone()).await; match line { Ok(line) => { @@ -315,27 +372,6 @@ pub async fn run( evaluate_response }; - let is_closing = post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.evaluate", - Some(json!({ - "expression": "(globalThis.closed)", - "contextId": context_id, - })), - ) - .await? - .get("result") - .unwrap() - .get("value") - .unwrap() - .as_bool() - .unwrap(); - - if is_closing { - break; - } - let evaluate_result = evaluate_response.get("result").unwrap(); let evaluate_exception_details = evaluate_response.get("exceptionDetails"); @@ -387,21 +423,19 @@ pub async fn run( let inspect_result = inspect_response.get("result").unwrap(); - match evaluate_exception_details { - Some(_) => eprintln!( - "Uncaught {}", - inspect_result.get("value").unwrap().as_str().unwrap() - ), - None => println!( - "{}", - inspect_result.get("value").unwrap().as_str().unwrap() - ), - } + let value = inspect_result.get("value").unwrap().as_str().unwrap(); + let output = match evaluate_exception_details { + Some(_) => format!("Uncaught {}", value), + None => value.to_string(), + }; + + println!("{}", output); editor.lock().unwrap().add_history_entry(line.as_str()); } Err(ReadlineError::Interrupted) => { - break; + println!("exit using ctrl+d or close()"); + continue; } Err(ReadlineError::Eof) => { break; diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index d375ba1a143aa..fdf2425cd672d 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1127,6 +1127,40 @@ fn run_watch() { drop(t); } +#[cfg(unix)] +#[test] +fn repl_test_pty_multiline() { + use std::io::Read; + use util::pty::fork::*; + + let tests_path = util::tests_path(); + let fork = Fork::from_ptmx().unwrap(); + if let Ok(mut master) = fork.is_parent() { + master.write_all(b"(\n1 + 2\n)\n").unwrap(); + master.write_all(b"{\nfoo: \"foo\"\n}\n").unwrap(); + master.write_all(b"`\nfoo\n`\n").unwrap(); + master.write_all(b"close();\n").unwrap(); + + let mut output = String::new(); + master.read_to_string(&mut output).unwrap(); + + assert!(output.contains('3')); + assert!(output.contains("{ foo: \"foo\" }")); + assert!(output.contains("\"\\nfoo\\n\"")); + + fork.wait().unwrap(); + } else { + util::deno_cmd() + .current_dir(tests_path) + .env("NO_COLOR", "1") + .arg("repl") + .spawn() + .unwrap() + .wait() + .unwrap(); + } +} + #[test] fn repl_test_console_log() { let (out, err) = util::run_and_collect_output( @@ -1232,7 +1266,7 @@ fn repl_test_eof() { #[test] fn repl_test_strict() { - let (_, err) = util::run_and_collect_output( + let (out, err) = util::run_and_collect_output( true, "repl", Some(vec![ @@ -1243,13 +1277,12 @@ fn repl_test_strict() { None, false, ); - assert!(err.contains( + assert!(out.contains( "Uncaught TypeError: Cannot add property c, object is not extensible" )); + assert!(err.is_empty()); } -const REPL_MSG: &str = "exit using ctrl+d or close()\n"; - #[test] fn repl_test_close_command() { let (out, err) = util::run_and_collect_output( @@ -1312,8 +1345,8 @@ fn repl_test_eval_unterminated() { None, false, ); - assert!(out.ends_with(REPL_MSG)); - assert!(err.contains("Unexpected end of input")); + assert!(out.contains("Unexpected end of input")); + assert!(err.is_empty()); } #[test] @@ -1325,8 +1358,8 @@ fn repl_test_reference_error() { None, false, ); - assert!(out.ends_with(REPL_MSG)); - assert!(err.contains("not_a_variable is not defined")); + assert!(out.contains("not_a_variable is not defined")); + assert!(err.is_empty()); } #[test] @@ -1338,8 +1371,8 @@ fn repl_test_syntax_error() { None, false, ); - assert!(out.ends_with(REPL_MSG)); - assert!(err.contains("Unexpected identifier")); + assert!(out.contains("Unexpected identifier")); + assert!(err.is_empty()); } #[test] @@ -1351,8 +1384,8 @@ fn repl_test_type_error() { None, false, ); - assert!(out.ends_with(REPL_MSG)); - assert!(err.contains("console is not a function")); + assert!(out.contains("console is not a function")); + assert!(err.is_empty()); } #[test] @@ -1425,8 +1458,8 @@ fn repl_test_save_last_thrown() { Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), false, ); - assert!(out.ends_with("1\n")); - assert_eq!(err, "Uncaught 1\n"); + assert!(out.ends_with("Uncaught 1\n1\n")); + assert!(err.is_empty()); } #[test] @@ -1453,10 +1486,11 @@ fn repl_test_assign_underscore_error() { Some(vec![("NO_COLOR".to_owned(), "1".to_owned())]), false, ); - assert!( - out.ends_with("Last thrown error is no longer saved to _error.\n1\n1\n") - ); - assert_eq!(err, "Uncaught 2\n"); + println!("{}", out); + assert!(out.ends_with( + "Last thrown error is no longer saved to _error.\n1\nUncaught 2\n1\n" + )); + assert!(err.is_empty()); } #[test] diff --git a/cli/tests/lint/expected_quiet.out b/cli/tests/lint/expected_quiet.out index 45a3083445218..21fd92be3ecd1 100644 --- a/cli/tests/lint/expected_quiet.out +++ b/cli/tests/lint/expected_quiet.out @@ -3,6 +3,8 @@ ^^^^^^^^^^^^^^^^^^^ at [WILDCARD]file1.js:1:0 + hint: [WILDCARD] + (no-empty) Empty block statement while (false) {} ^^ diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts index 64309c26947db..ab92997116aea 100644 --- a/cli/tests/unit/fetch_test.ts +++ b/cli/tests/unit/fetch_test.ts @@ -133,6 +133,7 @@ unitTest({ perms: { net: true } }, async function fetchAsyncIterator(): Promise< assert(response.body !== null); let total = 0; for await (const chunk of response.body) { + assert(chunk instanceof Uint8Array); total += chunk.length; } @@ -145,12 +146,13 @@ unitTest({ perms: { net: true } }, async function fetchBodyReader(): Promise< const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); const headers = response.headers; assert(response.body !== null); - const reader = await response.body.getReader(); + const reader = response.body.getReader(); let total = 0; while (true) { const { done, value } = await reader.read(); if (done) break; assert(value); + assert(value instanceof Uint8Array); total += value.length; } diff --git a/core/Cargo.toml b/core/Cargo.toml index c114a64d0b0c4..997306b245dde 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ # Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. [package] name = "deno_core" -version = "0.63.0" +version = "0.64.0" edition = "2018" description = "A secure JavaScript/TypeScript runtime built with V8, Rust, and Tokio" authors = ["the Deno authors"] @@ -19,7 +19,7 @@ indexmap = "1.6.0" lazy_static = "1.4.0" libc = "0.2.77" log = "0.4.11" -rusty_v8 = "0.11.0" +rusty_v8 = "0.12.0" serde_json = { version = "1.0", features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } smallvec = "1.4.2" diff --git a/core/bindings.rs b/core/bindings.rs index 782c98bd12e53..071d6e015f6e4 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -297,7 +297,7 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { match message.get_event() { v8::PromiseRejectEvent::PromiseRejectWithNoHandler => { - let error = message.get_value(); + let error = message.get_value().unwrap(); let error_global = v8::Global::new(scope, error); state .pending_promise_exceptions diff --git a/docs/getting_started/webassembly.md b/docs/getting_started/webassembly.md index b7ca6ee407e5c..c1f734fb6d1d4 100644 --- a/docs/getting_started/webassembly.md +++ b/docs/getting_started/webassembly.md @@ -16,5 +16,5 @@ const wasmCode = new Uint8Array([ const wasmModule = new WebAssembly.Module(wasmCode); const wasmInstance = new WebAssembly.Instance(wasmModule); const main = wasmInstance.exports.main as CallableFunction -console.log(wasmInstance.exports.main().toString()); +console.log(main().toString()); ``` diff --git a/docs/tools.md b/docs/tools.md index e64c9d840b779..5945ee0159665 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -7,5 +7,6 @@ and TypeScript: - [dependency inspector (`deno info`)](./tools/dependency_inspector.md) - [documentation generator (`deno doc`)](./tools/documentation_generator.md) - [formatter (`deno fmt`)](./tools/formatter.md) +- [repl (`deno repl`)](./tools/repl.md) - [test runner (`deno test`)](./testing.md) - [linter (`deno lint`)](./tools/linter.md) diff --git a/docs/tools/repl.md b/docs/tools/repl.md new file mode 100644 index 0000000000000..5b684b386d217 --- /dev/null +++ b/docs/tools/repl.md @@ -0,0 +1,45 @@ +# Read-eval-print-loop + +`deno repl` starts an read-eval-print-loop, which lets you interactively build +up program state in the global context. + +## Keyboard shortcuts + +| Keystroke | Action | +| --------------------- | ------------------------------------------------------------------------------------------------ | +| Ctrl-A, Home | Move cursor to the beginning of line | +| Ctrl-B, Left | Move cursor one character left | +| Ctrl-C | Interrupt and cancel the current edit | +| Ctrl-D | If if line _is_ empty, signal end of line | +| Ctrl-D, Del | If line is _not_ empty, delete character under cursor | +| Ctrl-E, End | Move cursor to end of line | +| Ctrl-F, Right | Move cursor one character right | +| Ctrl-H, Backspace | Delete character before cursor | +| Ctrl-I, Tab | Next completion | +| Ctrl-J, Ctrl-M, Enter | Finish the line entry | +| Ctrl-K | Delete from cursor to end of line | +| Ctrl-L | Clear screen | +| Ctrl-N, Down | Next match from history | +| Ctrl-P, Up | Previous match from history | +| Ctrl-R | Reverse Search history (Ctrl-S forward, Ctrl-G cancel) | +| Ctrl-T | Transpose previous character with current character | +| Ctrl-U | Delete from start of line to cursor | +| Ctrl-V | Insert any special character without performing its associated action (#65) | +| Ctrl-W | Delete word leading up to cursor (using white space as a word boundary) | +| Ctrl-X Ctrl-U | Undo | +| Ctrl-Y | Paste from Yank buffer | +| Ctrl-Y | Paste from Yank buffer (Meta-Y to paste next yank instead) | +| Ctrl-Z | Suspend (Unix only) | +| Ctrl-_ | Undo | +| Meta-0, 1, ..., - | Specify the digit to the argument. `–` starts a negative argument. | +| Meta-< | Move to first entry in history | +| Meta-> | Move to last entry in history | +| Meta-B, Alt-Left | Move cursor to previous word | +| Meta-Backspace | Kill from the start of the current word, or, if between words, to the start of the previous word | +| Meta-C | Capitalize the current word | +| Meta-D | Delete forwards one word | +| Meta-F, Alt-Right | Move cursor to next word | +| Meta-L | Lower-case the next word | +| Meta-T | Transpose words | +| Meta-U | Upper-case the next word | +| Meta-Y | See Ctrl-Y | diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js index 88744981b52de..887e329f9d5bf 100644 --- a/op_crates/fetch/26_fetch.js +++ b/op_crates/fetch/26_fetch.js @@ -786,7 +786,7 @@ this._stream = new ReadableStream({ start(controller) { - controller.enqueue(buf); + controller.enqueue(new Uint8Array(buf)); controller.close(); }, }); diff --git a/op_crates/fetch/Cargo.toml b/op_crates/fetch/Cargo.toml index 79197e52832a8..5e6c725b0f985 100644 --- a/op_crates/fetch/Cargo.toml +++ b/op_crates/fetch/Cargo.toml @@ -14,6 +14,6 @@ repository = "https://github.com/denoland/deno" path = "lib.rs" [dependencies] -deno_core = { version = "0.63.0", path = "../../core" } +deno_core = { version = "0.64.0", path = "../../core" } reqwest = { version = "0.10.8", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli"] } serde = { version = "1.0.116", features = ["derive"] } diff --git a/op_crates/web/Cargo.toml b/op_crates/web/Cargo.toml index 7d0c3de6c76f2..51686efbb6c47 100644 --- a/op_crates/web/Cargo.toml +++ b/op_crates/web/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/denoland/deno" path = "lib.rs" [dependencies] -deno_core = { version = "0.63.0", path = "../../core" } +deno_core = { version = "0.64.0", path = "../../core" } idna = "0.2.0" serde = { version = "1.0.116", features = ["derive"] } diff --git a/std/archive/tar.ts b/std/archive/tar.ts index a169d5b4ba77f..0685ad8374f3b 100644 --- a/std/archive/tar.ts +++ b/std/archive/tar.ts @@ -485,7 +485,10 @@ class TarEntry implements Reader { entryBytesLeft, ); - if (entryBytesLeft <= 0) return null; + if (entryBytesLeft <= 0) { + this.#consumed = true; + return null; + } const block = new Uint8Array(bufSize); const n = await readBlock(this.#reader, block); @@ -493,9 +496,7 @@ class TarEntry implements Reader { this.#read += n || 0; if (n === null || bytesLeft <= 0) { - // FIXME(bartlomieju): this condition makes no sense - // deno-lint-ignore no-constant-condition - if (null) this.#consumed = true; + if (n === null) this.#consumed = true; return null; } diff --git a/std/archive/tar_test.ts b/std/archive/tar_test.ts index ff1ada4d52a0b..cec98face993b 100644 --- a/std/archive/tar_test.ts +++ b/std/archive/tar_test.ts @@ -113,7 +113,9 @@ Deno.test("appendFileWithLongNameToTarArchive", async function (): Promise< const untar = new Untar(tar.getReader()); const result = await untar.extract(); assert(result !== null); + assert(!result.consumed); const untarText = new TextDecoder("utf-8").decode(await Deno.readAll(result)); + assert(result.consumed); // tests assertEquals(result.fileName, fileName); @@ -137,6 +139,7 @@ Deno.test("untarAsyncIterator", async function (): Promise { // read data from a tar archive const untar = new Untar(tar.getReader()); + let lastEntry; for await (const entry of untar) { const expected = entries.shift(); assert(expected); @@ -145,11 +148,14 @@ Deno.test("untarAsyncIterator", async function (): Promise { if (expected.filePath) { content = await Deno.readFile(expected.filePath); } - assertEquals(content, await Deno.readAll(entry)); assertEquals(expected.name, entry.fileName); - } + if (lastEntry) assert(lastEntry.consumed); + lastEntry = entry; + } + assert(lastEntry); + assert(lastEntry.consumed); assertEquals(entries.length, 0); }); diff --git a/std/datetime/README.md b/std/datetime/README.md index 253da498c48d6..97f2b7c2493bd 100644 --- a/std/datetime/README.md +++ b/std/datetime/README.md @@ -32,6 +32,8 @@ are supported: - `'foo'` - quoted literal. - `./-` - unquoted literal. +## Methods + ### parse Takes an input `string` and a `formatString` to parse to a `date`. @@ -85,3 +87,102 @@ import { weekOfYear } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; weekOfYear(new Date("2020-12-28T03:24:00")); // Returns 53 ``` + +### toIMF + +Formats the given date to IMF date time format. (Reference: +https://tools.ietf.org/html/rfc7231#section-7.1.1.1 ) + +```js +import { toIMF } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +toIMF(new Date(0)); // => returns "Thu, 01 Jan 1970 00:00:00 GMT" +``` + +### isLeap + +Returns true if the given date or year (in number) is a leap year. Returns false +otherwise. + +```js +import { isLeap } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +isLeap(new Date("1970-01-01")); // => returns false +isLeap(new Date("1972-01-01")); // => returns true +isLeap(new Date("2000-01-01")); // => returns true +isLeap(new Date("2100-01-01")); // => returns false +isLeap(1972); // => returns true +``` + +### difference + +Returns the difference of the 2 given dates in the given units. If the units are +omitted, it returns the difference in the all available units. + +Available units: "milliseconds", "seconds", "minutes", "hours", "days", "weeks", +"months", "quarters", "years" + +```js +import { difference } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +const date0 = new Date("2018-05-14"); +const date1 = new Date("2020-05-13"); + +difference(date0, date1, { units: ["days", "months", "years"] }); +// => returns { days: 730, months: 23, years: 1 } + +difference(date0, date1); +// => returns { +// milliseconds: 63072000000, +// seconds: 63072000, +// minutes: 1051200, +// hours: 17520, +// days: 730, +// weeks: 104, +// months: 23, +// quarters: 5, +// years: 1 +// } +``` + +## Constants + +### SECOND + +``` +import { SECOND } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +console.log(SECOND); // => 1000 +``` + +### MINUTE + +``` +import { MINUTE } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +console.log(MINUTE); // => 60000 (60 * 1000) +``` + +### HOUR + +``` +import { HOUR } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +console.log(HOUR); // => 3600000 (60 * 60 * 1000) +``` + +### DAY + +``` +import { DAY } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +console.log(DAY); // => 86400000 (24 * 60 * 60 * 1000) +``` + +### WEEK + +``` +import { WEEK } from "https://deno.land/std@$STD_VERSION/datetime/mod.ts"; + +console.log(WEEK); // => 604800000 (7 * 24 * 60 * 60 * 1000) +``` diff --git a/std/io/ioutil_test.ts b/std/io/ioutil_test.ts index 69f76f6913849..9b17ce66dae3d 100644 --- a/std/io/ioutil_test.ts +++ b/std/io/ioutil_test.ts @@ -9,8 +9,6 @@ import { } from "./ioutil.ts"; import { StringReader } from "./readers.ts"; import { BufReader } from "./bufio.ts"; -import { tempFile } from "./util.ts"; -import * as path from "../path/mod.ts"; class BinaryReader implements Deno.Reader { index = 0; @@ -87,7 +85,10 @@ Deno.test("testCopyN2", async function (): Promise { }); Deno.test("copyNWriteAllData", async function (): Promise { - const { filepath, file } = await tempFile(path.resolve("io")); + const tmpDir = await Deno.makeTempDir(); + const filepath = `${tmpDir}/data`; + const file = await Deno.open(filepath, { create: true, write: true }); + const size = 16 * 1024 + 1; const data = "a".repeat(32 * 1024); const r = new StringReader(data); diff --git a/std/path/mod.ts b/std/path/mod.ts index 0b4156e69981e..58c2c456104fa 100644 --- a/std/path/mod.ts +++ b/std/path/mod.ts @@ -24,6 +24,7 @@ export const { relative, resolve, sep, + toFileUrl, toNamespacedPath, } = path; diff --git a/std/path/posix.ts b/std/path/posix.ts index 68ffb06c9ac84..d2845f3da5f0a 100644 --- a/std/path/posix.ts +++ b/std/path/posix.ts @@ -440,3 +440,16 @@ export function fromFileUrl(url: string | URL): string { url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), ); } + +/** Converts a path string to a file URL. + * + * toFileUrl("/home/foo"); // new URL("file:///home/foo") + */ +export function toFileUrl(path: string): URL { + if (!isAbsolute(path)) { + throw new TypeError("Must be an absolute path."); + } + const url = new URL("file:///"); + url.pathname = path.replace(/%/g, "%25").replace(/\\/g, "%5C"); + return url; +} diff --git a/std/path/to_file_url_test.ts b/std/path/to_file_url_test.ts new file mode 100644 index 0000000000000..6d0ca8d809f78 --- /dev/null +++ b/std/path/to_file_url_test.ts @@ -0,0 +1,49 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { posix, win32 } from "./mod.ts"; +import { assertEquals, assertThrows } from "../testing/asserts.ts"; + +Deno.test("[path] toFileUrl", function () { + assertEquals(posix.toFileUrl("/home/foo").href, "file:///home/foo"); + assertEquals(posix.toFileUrl("/home/ ").href, "file:///home/%20"); + assertEquals(posix.toFileUrl("/home/%20").href, "file:///home/%2520"); + assertEquals(posix.toFileUrl("/home\\foo").href, "file:///home%5Cfoo"); + assertThrows( + () => posix.toFileUrl("foo").href, + TypeError, + "Must be an absolute path.", + ); + assertThrows( + () => posix.toFileUrl("C:/"), + TypeError, + "Must be an absolute path.", + ); + assertEquals( + posix.toFileUrl("//localhost/home/foo").href, + "file:////localhost/home/foo", + ); + assertEquals(posix.toFileUrl("//localhost/").href, "file:////localhost/"); + assertEquals(posix.toFileUrl("//:/home/foo").href, "file:////:/home/foo"); +}); + +Deno.test("[path] toFileUrl (win32)", function () { + assertEquals(win32.toFileUrl("/home/foo").href, "file:///home/foo"); + assertEquals(win32.toFileUrl("/home/ ").href, "file:///home/%20"); + assertEquals(win32.toFileUrl("/home/%20").href, "file:///home/%2520"); + assertEquals(win32.toFileUrl("/home\\foo").href, "file:///home/foo"); + assertThrows( + () => win32.toFileUrl("foo").href, + TypeError, + "Must be an absolute path.", + ); + assertEquals(win32.toFileUrl("C:/").href, "file:///C:/"); + assertEquals( + win32.toFileUrl("//localhost/home/foo").href, + "file://localhost/home/foo", + ); + assertEquals(win32.toFileUrl("//localhost/").href, "file:////localhost/"); + assertThrows( + () => win32.toFileUrl("//:/home/foo").href, + TypeError, + "Invalid hostname.", + ); +}); diff --git a/std/path/win32.ts b/std/path/win32.ts index 246c18a973f81..1684a2c45b6c4 100644 --- a/std/path/win32.ts +++ b/std/path/win32.ts @@ -917,11 +917,8 @@ export function fromFileUrl(url: string | URL): string { throw new TypeError("Must be a file URL."); } let path = decodeURIComponent( - url.pathname - .replace(/^\/*([A-Za-z]:)(\/|$)/, "$1/") - .replace(/\//g, "\\") - .replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), - ); + url.pathname.replace(/\//g, "\\").replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), + ).replace(/^\\*([A-Za-z]:)(\\|$)/, "$1\\"); if (url.hostname != "") { // Note: The `URL` implementation guarantees that the drive letter and // hostname are mutually exclusive. Otherwise it would not have been valid @@ -930,3 +927,27 @@ export function fromFileUrl(url: string | URL): string { } return path; } + +/** Converts a path string to a file URL. + * + * toFileUrl("\\home\\foo"); // new URL("file:///home/foo") + * toFileUrl("C:\\Users\\foo"); // new URL("file:///C:/Users/foo") + * toFileUrl("\\\\localhost\\home\\foo"); // new URL("file://localhost/home/foo") + */ +export function toFileUrl(path: string): URL { + if (!isAbsolute(path)) { + throw new TypeError("Must be an absolute path."); + } + const [, hostname, pathname] = path.match( + /^(?:[/\\]{2}([^/\\]+)(?=[/\\][^/\\]))?(.*)/, + )!; + const url = new URL("file:///"); + url.pathname = pathname.replace(/%/g, "%25"); + if (hostname != null) { + url.hostname = hostname; + if (!url.hostname) { + throw new TypeError("Invalid hostname."); + } + } + return url; +}