mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 20:29:11 +00:00
feat(unstable): support bytes and text imports in deno compile
(#29924)
Also includes: - https://github.com/denoland/deno_graph/pull/593 Closes https://github.com/denoland/deno/issues/29903 Closes https://github.com/denoland/deno/issues/29927
This commit is contained in:
parent
b5e41f605d
commit
8fcbb0fa43
26 changed files with 328 additions and 24 deletions
|
@ -43,6 +43,7 @@
|
||||||
"tests/node_compat/test",
|
"tests/node_compat/test",
|
||||||
"tests/registry/",
|
"tests/registry/",
|
||||||
"tests/specs/bench/default_ts",
|
"tests/specs/bench/default_ts",
|
||||||
|
"tests/specs/compile/bytes_and_text_imports",
|
||||||
"tests/specs/fmt",
|
"tests/specs/fmt",
|
||||||
"tests/specs/lint/bom",
|
"tests/specs/lint/bom",
|
||||||
"tests/specs/lint/default_ts",
|
"tests/specs/lint/default_ts",
|
||||||
|
@ -50,7 +51,6 @@
|
||||||
"tests/specs/publish/no_check_surfaces_syntax_error",
|
"tests/specs/publish/no_check_surfaces_syntax_error",
|
||||||
"tests/specs/run/default_ts",
|
"tests/specs/run/default_ts",
|
||||||
"tests/specs/test/default_ts",
|
"tests/specs/test/default_ts",
|
||||||
"tests/testdata/byte_order_mark.ts",
|
|
||||||
"tests/testdata/encoding",
|
"tests/testdata/encoding",
|
||||||
"tests/testdata/file_extensions/ts_with_js_extension.js",
|
"tests/testdata/file_extensions/ts_with_js_extension.js",
|
||||||
"tests/testdata/fmt/",
|
"tests/testdata/fmt/",
|
||||||
|
@ -61,6 +61,7 @@
|
||||||
"tests/testdata/lint/glob/",
|
"tests/testdata/lint/glob/",
|
||||||
"tests/testdata/malformed_config/",
|
"tests/testdata/malformed_config/",
|
||||||
"tests/testdata/run/byte_order_mark.ts",
|
"tests/testdata/run/byte_order_mark.ts",
|
||||||
|
"tests/testdata/run/invalid_utf8.ts",
|
||||||
"tests/testdata/run/error_syntax_empty_trailing_line.mjs",
|
"tests/testdata/run/error_syntax_empty_trailing_line.mjs",
|
||||||
"tests/testdata/run/inline_js_source_map*",
|
"tests/testdata/run/inline_js_source_map*",
|
||||||
"tests/testdata/test/markdown_windows.md",
|
"tests/testdata/test/markdown_windows.md",
|
||||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2062,9 +2062,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deno_graph"
|
name = "deno_graph"
|
||||||
version = "0.96.0"
|
version = "0.96.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c8d829b56e6cbd4176981234d1cffe285e0f8a680edbc0cdcfab91db018748c"
|
checksum = "0733ed99295ebeddeb0fb33efa41ccd47ccd9481ab1ebe01f2ea8af80204479e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"boxed_error",
|
"boxed_error",
|
||||||
|
|
|
@ -64,7 +64,7 @@ deno_core = { version = "0.352.0" }
|
||||||
deno_cache_dir = "=0.23.0"
|
deno_cache_dir = "=0.23.0"
|
||||||
deno_doc = "=0.179.0"
|
deno_doc = "=0.179.0"
|
||||||
deno_error = "=0.6.1"
|
deno_error = "=0.6.1"
|
||||||
deno_graph = { version = "=0.96.0", default-features = false }
|
deno_graph = { version = "=0.96.2", default-features = false }
|
||||||
deno_lint = "=0.76.0"
|
deno_lint = "=0.76.0"
|
||||||
deno_lockfile = "=0.30.1"
|
deno_lockfile = "=0.30.1"
|
||||||
deno_media_type = { version = "=0.2.9", features = ["module_specifier"] }
|
deno_media_type = { version = "=0.2.9", features = ["module_specifier"] }
|
||||||
|
|
|
@ -964,6 +964,7 @@ impl CliFactory {
|
||||||
self.cli_options()?,
|
self.cli_options()?,
|
||||||
self.deno_dir()?,
|
self.deno_dir()?,
|
||||||
self.emitter()?,
|
self.emitter()?,
|
||||||
|
self.file_fetcher()?,
|
||||||
self.http_client_provider(),
|
self.http_client_provider(),
|
||||||
self.npm_resolver().await?,
|
self.npm_resolver().await?,
|
||||||
self.workspace_resolver().await?.as_ref(),
|
self.workspace_resolver().await?.as_ref(),
|
||||||
|
|
|
@ -136,9 +136,11 @@ pub enum CjsExportAnalysisEntry {
|
||||||
const HAS_TRANSPILED_FLAG: u8 = 1 << 0;
|
const HAS_TRANSPILED_FLAG: u8 = 1 << 0;
|
||||||
const HAS_SOURCE_MAP_FLAG: u8 = 1 << 1;
|
const HAS_SOURCE_MAP_FLAG: u8 = 1 << 1;
|
||||||
const HAS_CJS_EXPORT_ANALYSIS_FLAG: u8 = 1 << 2;
|
const HAS_CJS_EXPORT_ANALYSIS_FLAG: u8 = 1 << 2;
|
||||||
|
const HAS_VALID_UTF8_FLAG: u8 = 1 << 3;
|
||||||
|
|
||||||
pub struct RemoteModuleEntry<'a> {
|
pub struct RemoteModuleEntry<'a> {
|
||||||
pub media_type: MediaType,
|
pub media_type: MediaType,
|
||||||
|
pub is_valid_utf8: bool,
|
||||||
pub data: Cow<'a, [u8]>,
|
pub data: Cow<'a, [u8]>,
|
||||||
pub maybe_transpiled: Option<Cow<'a, [u8]>>,
|
pub maybe_transpiled: Option<Cow<'a, [u8]>>,
|
||||||
pub maybe_source_map: Option<Cow<'a, [u8]>>,
|
pub maybe_source_map: Option<Cow<'a, [u8]>>,
|
||||||
|
@ -161,6 +163,9 @@ impl<'a> DenoRtSerializable<'a> for RemoteModuleEntry<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut has_data_flags = 0;
|
let mut has_data_flags = 0;
|
||||||
|
if self.is_valid_utf8 {
|
||||||
|
has_data_flags |= HAS_VALID_UTF8_FLAG;
|
||||||
|
}
|
||||||
if self.maybe_transpiled.is_some() {
|
if self.maybe_transpiled.is_some() {
|
||||||
has_data_flags |= HAS_TRANSPILED_FLAG;
|
has_data_flags |= HAS_TRANSPILED_FLAG;
|
||||||
}
|
}
|
||||||
|
@ -203,6 +208,7 @@ impl<'a> DenoRtDeserializable<'a> for RemoteModuleEntry<'a> {
|
||||||
deserialize_data_if_has_flag(input, has_data_flags, HAS_TRANSPILED_FLAG)?;
|
deserialize_data_if_has_flag(input, has_data_flags, HAS_TRANSPILED_FLAG)?;
|
||||||
let (input, maybe_source_map) =
|
let (input, maybe_source_map) =
|
||||||
deserialize_data_if_has_flag(input, has_data_flags, HAS_SOURCE_MAP_FLAG)?;
|
deserialize_data_if_has_flag(input, has_data_flags, HAS_SOURCE_MAP_FLAG)?;
|
||||||
|
let is_valid_utf8 = has_data_flags & HAS_VALID_UTF8_FLAG != 0;
|
||||||
let (input, maybe_cjs_export_analysis) = deserialize_data_if_has_flag(
|
let (input, maybe_cjs_export_analysis) = deserialize_data_if_has_flag(
|
||||||
input,
|
input,
|
||||||
has_data_flags,
|
has_data_flags,
|
||||||
|
@ -213,6 +219,7 @@ impl<'a> DenoRtDeserializable<'a> for RemoteModuleEntry<'a> {
|
||||||
Self {
|
Self {
|
||||||
media_type,
|
media_type,
|
||||||
data: Cow::Borrowed(data),
|
data: Cow::Borrowed(data),
|
||||||
|
is_valid_utf8,
|
||||||
maybe_transpiled,
|
maybe_transpiled,
|
||||||
maybe_source_map,
|
maybe_source_map,
|
||||||
maybe_cjs_export_analysis,
|
maybe_cjs_export_analysis,
|
||||||
|
|
|
@ -26,6 +26,8 @@ use serde::Deserializer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::Serializer;
|
use serde::Serializer;
|
||||||
|
|
||||||
|
use crate::util::text_encoding::is_valid_utf8;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum WindowsSystemRootablePath {
|
pub enum WindowsSystemRootablePath {
|
||||||
/// The root of the system above any drive letters.
|
/// The root of the system above any drive letters.
|
||||||
|
@ -257,6 +259,8 @@ pub struct VirtualFile {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(rename = "o")]
|
#[serde(rename = "o")]
|
||||||
pub offset: OffsetWithLength,
|
pub offset: OffsetWithLength,
|
||||||
|
#[serde(default, rename = "u", skip_serializing_if = "is_false")]
|
||||||
|
pub is_valid_utf8: bool,
|
||||||
#[serde(rename = "m", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "m", skip_serializing_if = "Option::is_none")]
|
||||||
pub transpiled_offset: Option<OffsetWithLength>,
|
pub transpiled_offset: Option<OffsetWithLength>,
|
||||||
#[serde(rename = "c", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "c", skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -267,6 +271,10 @@ pub struct VirtualFile {
|
||||||
pub mtime: Option<u128>, // mtime in milliseconds
|
pub mtime: Option<u128>, // mtime in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_false(value: &bool) -> bool {
|
||||||
|
!value
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct VirtualSymlinkParts(Vec<String>);
|
pub struct VirtualSymlinkParts(Vec<String>);
|
||||||
|
|
||||||
|
@ -766,6 +774,7 @@ impl VfsBuilder {
|
||||||
log::debug!("Adding file '{}'", path.display());
|
log::debug!("Adding file '{}'", path.display());
|
||||||
let case_sensitivity = self.case_sensitivity;
|
let case_sensitivity = self.case_sensitivity;
|
||||||
|
|
||||||
|
let is_valid_utf8 = is_valid_utf8(&options.data);
|
||||||
let offset_and_len = self.files.add_data(options.data);
|
let offset_and_len = self.files.add_data(options.data);
|
||||||
let transpiled_offset = options
|
let transpiled_offset = options
|
||||||
.maybe_transpiled
|
.maybe_transpiled
|
||||||
|
@ -790,6 +799,7 @@ impl VfsBuilder {
|
||||||
|| {
|
|| {
|
||||||
VfsEntry::File(VirtualFile {
|
VfsEntry::File(VirtualFile {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
is_valid_utf8,
|
||||||
offset: offset_and_len,
|
offset: offset_and_len,
|
||||||
transpiled_offset,
|
transpiled_offset,
|
||||||
cjs_export_analysis_offset,
|
cjs_export_analysis_offset,
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn is_valid_utf8(bytes: &[u8]) -> bool {
|
||||||
|
matches!(String::from_utf8_lossy(bytes), Cow::Borrowed(_))
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_utf8_lossy_owned(bytes: Vec<u8>) -> String {
|
pub fn from_utf8_lossy_owned(bytes: Vec<u8>) -> String {
|
||||||
match String::from_utf8_lossy(&bytes) {
|
match String::from_utf8_lossy(&bytes) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ use deno_core::error::AnyError;
|
||||||
use deno_core::serde_json;
|
use deno_core::serde_json;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_core::FastString;
|
use deno_core::FastString;
|
||||||
|
use deno_core::ModuleCodeBytes;
|
||||||
use deno_core::ModuleSourceCode;
|
use deno_core::ModuleSourceCode;
|
||||||
use deno_core::ModuleType;
|
use deno_core::ModuleType;
|
||||||
use deno_error::JsError;
|
use deno_error::JsError;
|
||||||
|
@ -348,12 +349,14 @@ impl StandaloneModules {
|
||||||
let mut transpiled = None;
|
let mut transpiled = None;
|
||||||
let mut source_map = None;
|
let mut source_map = None;
|
||||||
let mut cjs_export_analysis = None;
|
let mut cjs_export_analysis = None;
|
||||||
|
let mut is_valid_utf8 = false;
|
||||||
let bytes = match self.vfs.file_entry(&path) {
|
let bytes = match self.vfs.file_entry(&path) {
|
||||||
Ok(entry) => {
|
Ok(entry) => {
|
||||||
let bytes = self
|
let bytes = self
|
||||||
.vfs
|
.vfs
|
||||||
.read_file_all(entry)
|
.read_file_all(entry)
|
||||||
.map_err(JsErrorBox::from_err)?;
|
.map_err(JsErrorBox::from_err)?;
|
||||||
|
is_valid_utf8 = entry.is_valid_utf8;
|
||||||
transpiled = entry
|
transpiled = entry
|
||||||
.transpiled_offset
|
.transpiled_offset
|
||||||
.and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
|
.and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
|
||||||
|
@ -379,6 +382,7 @@ impl StandaloneModules {
|
||||||
Ok(Some(DenoCompileModuleData {
|
Ok(Some(DenoCompileModuleData {
|
||||||
media_type: MediaType::from_specifier(specifier),
|
media_type: MediaType::from_specifier(specifier),
|
||||||
specifier,
|
specifier,
|
||||||
|
is_valid_utf8,
|
||||||
data: bytes,
|
data: bytes,
|
||||||
transpiled,
|
transpiled,
|
||||||
source_map,
|
source_map,
|
||||||
|
@ -393,6 +397,7 @@ impl StandaloneModules {
|
||||||
pub struct DenoCompileModuleData<'a> {
|
pub struct DenoCompileModuleData<'a> {
|
||||||
pub specifier: &'a Url,
|
pub specifier: &'a Url,
|
||||||
pub media_type: MediaType,
|
pub media_type: MediaType,
|
||||||
|
pub is_valid_utf8: bool,
|
||||||
pub data: Cow<'static, [u8]>,
|
pub data: Cow<'static, [u8]>,
|
||||||
pub transpiled: Option<Cow<'static, [u8]>>,
|
pub transpiled: Option<Cow<'static, [u8]>>,
|
||||||
pub source_map: Option<Cow<'static, [u8]>>,
|
pub source_map: Option<Cow<'static, [u8]>>,
|
||||||
|
@ -401,12 +406,18 @@ pub struct DenoCompileModuleData<'a> {
|
||||||
|
|
||||||
impl<'a> DenoCompileModuleData<'a> {
|
impl<'a> DenoCompileModuleData<'a> {
|
||||||
pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) {
|
pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) {
|
||||||
fn into_string_unsafe(data: Cow<'static, [u8]>) -> DenoCompileModuleSource {
|
fn into_string_unsafe(
|
||||||
|
is_valid_utf8: bool,
|
||||||
|
data: Cow<'static, [u8]>,
|
||||||
|
) -> DenoCompileModuleSource {
|
||||||
match data {
|
match data {
|
||||||
Cow::Borrowed(d) => DenoCompileModuleSource::String(
|
Cow::Borrowed(d) if is_valid_utf8 => {
|
||||||
|
DenoCompileModuleSource::String(
|
||||||
// SAFETY: we know this is a valid utf8 string
|
// SAFETY: we know this is a valid utf8 string
|
||||||
unsafe { std::str::from_utf8_unchecked(d) },
|
unsafe { std::str::from_utf8_unchecked(d) },
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
Cow::Borrowed(_) => DenoCompileModuleSource::Bytes(data),
|
||||||
Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)),
|
Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,8 +434,14 @@ impl<'a> DenoCompileModuleData<'a> {
|
||||||
| MediaType::Dts
|
| MediaType::Dts
|
||||||
| MediaType::Dmts
|
| MediaType::Dmts
|
||||||
| MediaType::Dcts
|
| MediaType::Dcts
|
||||||
| MediaType::Tsx => (ModuleType::JavaScript, into_string_unsafe(data)),
|
| MediaType::Tsx => (
|
||||||
MediaType::Json => (ModuleType::Json, into_string_unsafe(data)),
|
ModuleType::JavaScript,
|
||||||
|
into_string_unsafe(self.is_valid_utf8, data),
|
||||||
|
),
|
||||||
|
MediaType::Json => (
|
||||||
|
ModuleType::Json,
|
||||||
|
into_string_unsafe(self.is_valid_utf8, data),
|
||||||
|
),
|
||||||
MediaType::Wasm => {
|
MediaType::Wasm => {
|
||||||
(ModuleType::Wasm, DenoCompileModuleSource::Bytes(data))
|
(ModuleType::Wasm, DenoCompileModuleSource::Bytes(data))
|
||||||
}
|
}
|
||||||
|
@ -441,6 +458,7 @@ impl<'a> DenoCompileModuleData<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum DenoCompileModuleSource {
|
pub enum DenoCompileModuleSource {
|
||||||
String(&'static str),
|
String(&'static str),
|
||||||
Bytes(Cow<'static, [u8]>),
|
Bytes(Cow<'static, [u8]>),
|
||||||
|
@ -448,21 +466,28 @@ pub enum DenoCompileModuleSource {
|
||||||
|
|
||||||
impl DenoCompileModuleSource {
|
impl DenoCompileModuleSource {
|
||||||
pub fn into_for_v8(self) -> ModuleSourceCode {
|
pub fn into_for_v8(self) -> ModuleSourceCode {
|
||||||
fn into_bytes(data: Cow<'static, [u8]>) -> ModuleSourceCode {
|
|
||||||
ModuleSourceCode::Bytes(match data {
|
|
||||||
Cow::Borrowed(d) => d.into(),
|
|
||||||
Cow::Owned(d) => d.into_boxed_slice().into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
// todo(https://github.com/denoland/deno_core/pull/943): store whether
|
// todo(https://github.com/denoland/deno_core/pull/943): store whether
|
||||||
// the string is ascii or not ahead of time so we can avoid the is_ascii()
|
// the string is ascii or not ahead of time so we can avoid the is_ascii()
|
||||||
// check in FastString::from_static
|
// check in FastString::from_static
|
||||||
Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)),
|
Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)),
|
||||||
Self::Bytes(b) => into_bytes(b),
|
Self::Bytes(b) => ModuleSourceCode::Bytes(module_source_into_bytes(b)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_bytes_for_v8(self) -> ModuleCodeBytes {
|
||||||
|
match self {
|
||||||
|
DenoCompileModuleSource::String(text) => text.as_bytes().into(),
|
||||||
|
DenoCompileModuleSource::Bytes(b) => module_source_into_bytes(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn module_source_into_bytes(data: Cow<'static, [u8]>) -> ModuleCodeBytes {
|
||||||
|
match data {
|
||||||
|
Cow::Borrowed(d) => d.into(),
|
||||||
|
Cow::Owned(d) => d.into_boxed_slice().into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, JsError)]
|
#[derive(Debug, Error, JsError)]
|
||||||
|
@ -558,6 +583,7 @@ impl RemoteModulesStore {
|
||||||
self.specifiers.get_specifier(specifier).unwrap()
|
self.specifiers.get_specifier(specifier).unwrap()
|
||||||
},
|
},
|
||||||
media_type: entry.media_type,
|
media_type: entry.media_type,
|
||||||
|
is_valid_utf8: entry.is_valid_utf8,
|
||||||
data: handle_cow_ref(&entry.data),
|
data: handle_cow_ref(&entry.data),
|
||||||
transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref),
|
transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref),
|
||||||
source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref),
|
source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref),
|
||||||
|
|
|
@ -17,6 +17,7 @@ use deno_core::v8_set_flags;
|
||||||
use deno_core::FastString;
|
use deno_core::FastString;
|
||||||
use deno_core::ModuleLoader;
|
use deno_core::ModuleLoader;
|
||||||
use deno_core::ModuleSourceCode;
|
use deno_core::ModuleSourceCode;
|
||||||
|
use deno_core::ModuleType;
|
||||||
use deno_core::RequestedModuleType;
|
use deno_core::RequestedModuleType;
|
||||||
use deno_core::ResolutionKind;
|
use deno_core::ResolutionKind;
|
||||||
use deno_core::SourceCodeCacheInfo;
|
use deno_core::SourceCodeCacheInfo;
|
||||||
|
@ -370,7 +371,7 @@ impl ModuleLoader for EmbeddedModuleLoader {
|
||||||
original_specifier: &Url,
|
original_specifier: &Url,
|
||||||
maybe_referrer: Option<&Url>,
|
maybe_referrer: Option<&Url>,
|
||||||
_is_dynamic: bool,
|
_is_dynamic: bool,
|
||||||
_requested_module_type: RequestedModuleType,
|
requested_module_type: RequestedModuleType,
|
||||||
) -> deno_core::ModuleLoadResponse {
|
) -> deno_core::ModuleLoadResponse {
|
||||||
if original_specifier.scheme() == "data" {
|
if original_specifier.scheme() == "data" {
|
||||||
let data_url_text =
|
let data_url_text =
|
||||||
|
@ -423,6 +424,36 @@ impl ModuleLoader for EmbeddedModuleLoader {
|
||||||
|
|
||||||
match self.shared.modules.read(original_specifier) {
|
match self.shared.modules.read(original_specifier) {
|
||||||
Ok(Some(module)) => {
|
Ok(Some(module)) => {
|
||||||
|
match requested_module_type {
|
||||||
|
RequestedModuleType::Text | RequestedModuleType::Bytes => {
|
||||||
|
let module_source = DenoCompileModuleSource::Bytes(module.data);
|
||||||
|
return deno_core::ModuleLoadResponse::Sync(Ok(
|
||||||
|
deno_core::ModuleSource::new_with_redirect(
|
||||||
|
match requested_module_type {
|
||||||
|
RequestedModuleType::Text => ModuleType::Text,
|
||||||
|
RequestedModuleType::Bytes => ModuleType::Bytes,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
match requested_module_type {
|
||||||
|
RequestedModuleType::Text => module_source.into_for_v8(),
|
||||||
|
RequestedModuleType::Bytes => {
|
||||||
|
ModuleSourceCode::Bytes(module_source.into_bytes_for_v8())
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
original_specifier,
|
||||||
|
module.specifier,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
RequestedModuleType::Other(_)
|
||||||
|
| RequestedModuleType::None
|
||||||
|
| RequestedModuleType::Json => {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let media_type = module.media_type;
|
let media_type = module.media_type;
|
||||||
let (module_specifier, module_type, module_source) =
|
let (module_specifier, module_type, module_source) =
|
||||||
module.into_parts();
|
module.into_parts();
|
||||||
|
|
|
@ -44,6 +44,7 @@ use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries;
|
||||||
use deno_lib::standalone::virtual_fs::WindowsSystemRootablePath;
|
use deno_lib::standalone::virtual_fs::WindowsSystemRootablePath;
|
||||||
use deno_lib::standalone::virtual_fs::DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME;
|
use deno_lib::standalone::virtual_fs::DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME;
|
||||||
use deno_lib::util::hash::FastInsecureHasher;
|
use deno_lib::util::hash::FastInsecureHasher;
|
||||||
|
use deno_lib::util::text_encoding::is_valid_utf8;
|
||||||
use deno_lib::util::v8::construct_v8_flags;
|
use deno_lib::util::v8::construct_v8_flags;
|
||||||
use deno_lib::version::DENO_VERSION_INFO;
|
use deno_lib::version::DENO_VERSION_INFO;
|
||||||
use deno_npm::resolution::SerializedNpmResolutionSnapshot;
|
use deno_npm::resolution::SerializedNpmResolutionSnapshot;
|
||||||
|
@ -51,6 +52,9 @@ use deno_npm::NpmSystemInfo;
|
||||||
use deno_path_util::fs::atomic_write_file_with_retries;
|
use deno_path_util::fs::atomic_write_file_with_retries;
|
||||||
use deno_path_util::url_from_directory_path;
|
use deno_path_util::url_from_directory_path;
|
||||||
use deno_path_util::url_to_file_path;
|
use deno_path_util::url_to_file_path;
|
||||||
|
use deno_resolver::file_fetcher::FetchLocalOptions;
|
||||||
|
use deno_resolver::file_fetcher::FetchOptions;
|
||||||
|
use deno_resolver::file_fetcher::FetchPermissionsOptionRef;
|
||||||
use deno_resolver::workspace::WorkspaceResolver;
|
use deno_resolver::workspace::WorkspaceResolver;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use node_resolver::analyze::ResolvedCjsAnalysis;
|
use node_resolver::analyze::ResolvedCjsAnalysis;
|
||||||
|
@ -61,6 +65,7 @@ use crate::args::CliOptions;
|
||||||
use crate::args::CompileFlags;
|
use crate::args::CompileFlags;
|
||||||
use crate::cache::DenoDir;
|
use crate::cache::DenoDir;
|
||||||
use crate::emit::Emitter;
|
use crate::emit::Emitter;
|
||||||
|
use crate::file_fetcher::CliFileFetcher;
|
||||||
use crate::http_util::HttpClientProvider;
|
use crate::http_util::HttpClientProvider;
|
||||||
use crate::node::CliCjsModuleExportAnalyzer;
|
use crate::node::CliCjsModuleExportAnalyzer;
|
||||||
use crate::npm::CliNpmResolver;
|
use crate::npm::CliNpmResolver;
|
||||||
|
@ -197,6 +202,7 @@ pub struct DenoCompileBinaryWriter<'a> {
|
||||||
cli_options: &'a CliOptions,
|
cli_options: &'a CliOptions,
|
||||||
deno_dir: &'a DenoDir,
|
deno_dir: &'a DenoDir,
|
||||||
emitter: &'a Emitter,
|
emitter: &'a Emitter,
|
||||||
|
file_fetcher: &'a CliFileFetcher,
|
||||||
http_client_provider: &'a HttpClientProvider,
|
http_client_provider: &'a HttpClientProvider,
|
||||||
npm_resolver: &'a CliNpmResolver,
|
npm_resolver: &'a CliNpmResolver,
|
||||||
workspace_resolver: &'a WorkspaceResolver<CliSys>,
|
workspace_resolver: &'a WorkspaceResolver<CliSys>,
|
||||||
|
@ -211,6 +217,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
cli_options: &'a CliOptions,
|
cli_options: &'a CliOptions,
|
||||||
deno_dir: &'a DenoDir,
|
deno_dir: &'a DenoDir,
|
||||||
emitter: &'a Emitter,
|
emitter: &'a Emitter,
|
||||||
|
file_fetcher: &'a CliFileFetcher,
|
||||||
http_client_provider: &'a HttpClientProvider,
|
http_client_provider: &'a HttpClientProvider,
|
||||||
npm_resolver: &'a CliNpmResolver,
|
npm_resolver: &'a CliNpmResolver,
|
||||||
workspace_resolver: &'a WorkspaceResolver<CliSys>,
|
workspace_resolver: &'a WorkspaceResolver<CliSys>,
|
||||||
|
@ -222,6 +229,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
cli_options,
|
cli_options,
|
||||||
deno_dir,
|
deno_dir,
|
||||||
emitter,
|
emitter,
|
||||||
|
file_fetcher,
|
||||||
http_client_provider,
|
http_client_provider,
|
||||||
npm_resolver,
|
npm_resolver,
|
||||||
workspace_resolver,
|
workspace_resolver,
|
||||||
|
@ -409,6 +417,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
let mut specifier_store = SpecifierStore::with_capacity(specifiers_count);
|
let mut specifier_store = SpecifierStore::with_capacity(specifiers_count);
|
||||||
let mut remote_modules_store =
|
let mut remote_modules_store =
|
||||||
SpecifierDataStore::with_capacity(specifiers_count);
|
SpecifierDataStore::with_capacity(specifiers_count);
|
||||||
|
let mut asset_module_urls = graph.asset_module_urls();
|
||||||
// todo(dsherret): transpile and analyze CJS in parallel
|
// todo(dsherret): transpile and analyze CJS in parallel
|
||||||
for module in graph.modules() {
|
for module in graph.modules() {
|
||||||
if module.specifier().scheme() == "data" {
|
if module.specifier().scheme() == "data" {
|
||||||
|
@ -420,7 +429,10 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
let (maybe_original_source, media_type) = match module {
|
let (maybe_original_source, media_type) = match module {
|
||||||
deno_graph::Module::Js(m) => {
|
deno_graph::Module::Js(m) => {
|
||||||
let specifier = &m.specifier;
|
let specifier = &m.specifier;
|
||||||
let original_bytes = m.source.text.as_bytes();
|
let original_bytes = match m.source.try_get_original_bytes() {
|
||||||
|
Some(bytes) => bytes,
|
||||||
|
None => self.load_asset_bypass_permissions(specifier).await?.source,
|
||||||
|
};
|
||||||
if self.cjs_tracker.is_maybe_cjs(specifier, m.media_type)? {
|
if self.cjs_tracker.is_maybe_cjs(specifier, m.media_type)? {
|
||||||
if self.cjs_tracker.is_cjs_with_known_is_script(
|
if self.cjs_tracker.is_cjs_with_known_is_script(
|
||||||
specifier,
|
specifier,
|
||||||
|
@ -466,16 +478,26 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
(Some(original_bytes), m.media_type)
|
(Some(original_bytes), m.media_type)
|
||||||
}
|
}
|
||||||
deno_graph::Module::Json(m) => {
|
deno_graph::Module::Json(m) => {
|
||||||
(Some(m.source.text.as_bytes()), m.media_type)
|
let original_bytes = match m.source.try_get_original_bytes() {
|
||||||
|
Some(bytes) => bytes,
|
||||||
|
None => {
|
||||||
|
self
|
||||||
|
.load_asset_bypass_permissions(&m.specifier)
|
||||||
|
.await?
|
||||||
|
.source
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Some(original_bytes), m.media_type)
|
||||||
}
|
}
|
||||||
deno_graph::Module::Wasm(m) => {
|
deno_graph::Module::Wasm(m) => {
|
||||||
(Some(m.source.as_ref()), MediaType::Wasm)
|
(Some(m.source.clone()), MediaType::Wasm)
|
||||||
}
|
}
|
||||||
deno_graph::Module::Npm(_)
|
deno_graph::Module::Npm(_)
|
||||||
| deno_graph::Module::Node(_)
|
| deno_graph::Module::Node(_)
|
||||||
| deno_graph::Module::External(_) => (None, MediaType::Unknown),
|
| deno_graph::Module::External(_) => (None, MediaType::Unknown),
|
||||||
};
|
};
|
||||||
if let Some(original_source) = maybe_original_source {
|
if let Some(original_source) = maybe_original_source {
|
||||||
|
asset_module_urls.swap_remove(module.specifier());
|
||||||
let maybe_cjs_export_analysis = maybe_cjs_analysis
|
let maybe_cjs_export_analysis = maybe_cjs_analysis
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(bincode::serialize)
|
.map(bincode::serialize)
|
||||||
|
@ -505,7 +527,8 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
specifier_id,
|
specifier_id,
|
||||||
RemoteModuleEntry {
|
RemoteModuleEntry {
|
||||||
media_type,
|
media_type,
|
||||||
data: Cow::Borrowed(original_source),
|
is_valid_utf8: is_valid_utf8(&original_source),
|
||||||
|
data: Cow::Owned(original_source.to_vec()),
|
||||||
maybe_transpiled: maybe_transpiled.map(Cow::Owned),
|
maybe_transpiled: maybe_transpiled.map(Cow::Owned),
|
||||||
maybe_source_map: maybe_source_map.map(Cow::Owned),
|
maybe_source_map: maybe_source_map.map(Cow::Owned),
|
||||||
maybe_cjs_export_analysis: maybe_cjs_export_analysis
|
maybe_cjs_export_analysis: maybe_cjs_export_analysis
|
||||||
|
@ -516,6 +539,42 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for url in asset_module_urls {
|
||||||
|
if graph.try_get(url).is_err() {
|
||||||
|
// skip because there was an error loading this module
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match url.scheme() {
|
||||||
|
"file" => {
|
||||||
|
let file_path = deno_path_util::url_to_file_path(url)?;
|
||||||
|
vfs.add_file_at_path(&file_path)?;
|
||||||
|
}
|
||||||
|
"http" | "https" => {
|
||||||
|
let specifier_id = specifier_store.get_or_add(url);
|
||||||
|
if !remote_modules_store.contains(specifier_id) {
|
||||||
|
// it's ok to bypass permissions here because we verified the module
|
||||||
|
// loaded successfully in the graph
|
||||||
|
let file = self.load_asset_bypass_permissions(url).await?;
|
||||||
|
remote_modules_store.add(
|
||||||
|
specifier_id,
|
||||||
|
RemoteModuleEntry {
|
||||||
|
media_type: MediaType::from_specifier_and_headers(
|
||||||
|
&file.url,
|
||||||
|
file.maybe_headers.as_ref(),
|
||||||
|
),
|
||||||
|
is_valid_utf8: is_valid_utf8(&file.source),
|
||||||
|
data: Cow::Owned(file.source.to_vec()),
|
||||||
|
maybe_cjs_export_analysis: None,
|
||||||
|
maybe_source_map: None,
|
||||||
|
maybe_transpiled: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut redirects_store =
|
let mut redirects_store =
|
||||||
SpecifierDataStore::with_capacity(graph.redirects.len());
|
SpecifierDataStore::with_capacity(graph.redirects.len());
|
||||||
for (from, to) in &graph.redirects {
|
for (from, to) in &graph.redirects {
|
||||||
|
@ -756,6 +815,32 @@ impl<'a> DenoCompileBinaryWriter<'a> {
|
||||||
.context("Writing binary bytes")
|
.context("Writing binary bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_asset_bypass_permissions(
|
||||||
|
&self,
|
||||||
|
specifier: &ModuleSpecifier,
|
||||||
|
) -> Result<
|
||||||
|
deno_cache_dir::file_fetcher::File,
|
||||||
|
deno_resolver::file_fetcher::FetchError,
|
||||||
|
> {
|
||||||
|
self
|
||||||
|
.file_fetcher
|
||||||
|
.fetch_with_options(
|
||||||
|
specifier,
|
||||||
|
FetchPermissionsOptionRef::AllowAll,
|
||||||
|
FetchOptions {
|
||||||
|
local: FetchLocalOptions {
|
||||||
|
include_mtime: false,
|
||||||
|
},
|
||||||
|
maybe_auth: None,
|
||||||
|
maybe_accept: None,
|
||||||
|
maybe_cache_setting: Some(
|
||||||
|
&deno_cache_dir::file_fetcher::CacheSetting::Use,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
fn fill_npm_vfs(&self, builder: &mut VfsBuilder) -> Result<(), AnyError> {
|
fn fill_npm_vfs(&self, builder: &mut VfsBuilder) -> Result<(), AnyError> {
|
||||||
fn maybe_warn_different_system(system_info: &NpmSystemInfo) {
|
fn maybe_warn_different_system(system_info: &NpmSystemInfo) {
|
||||||
if system_info != &NpmSystemInfo::default() {
|
if system_info != &NpmSystemInfo::default() {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"tempDir": true,
|
||||||
|
"steps": [{
|
||||||
|
"args": "compile --unstable-raw-imports --output bin main.ts",
|
||||||
|
"output": "compile.out",
|
||||||
|
"exitCode": 1
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
error: Requires import access to "localhost:4545", run again with the --allow-import flag
|
||||||
|
at file:///[WILDLINE]
|
|
@ -0,0 +1,3 @@
|
||||||
|
import bytes from "http://localhost:4545/run/invalid_utf8.ts" with { type: "text" };
|
||||||
|
|
||||||
|
console.log(bytes);
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"tempDir": true,
|
||||||
|
"steps": [{
|
||||||
|
"args": "compile --unstable-raw-imports --output bin main.ts",
|
||||||
|
"output": "compile.out"
|
||||||
|
}, {
|
||||||
|
"commandName": "./bin",
|
||||||
|
"args": [],
|
||||||
|
"output": "run.out",
|
||||||
|
"exitCode": 1
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import bytes from "http://localhost:4545/run/invalid_utf8.ts" with { type: "text" };
|
||||||
|
|
||||||
|
console.log(bytes);
|
|
@ -0,0 +1,11 @@
|
||||||
|
[# Warning! It should not show that it downloaded anything here]
|
||||||
|
Check file:///[WILDLINE]
|
||||||
|
Compile file:///[WILDLINE]
|
||||||
|
|
||||||
|
Embedded Files
|
||||||
|
|
||||||
|
[WILDLINE]
|
||||||
|
├── branch.ts ([WILDLINE])
|
||||||
|
└── main.ts ([WILDLINE])
|
||||||
|
|
||||||
|
[WILDCARD]
|
|
@ -0,0 +1 @@
|
||||||
|
await import("./branch.ts");
|
|
@ -0,0 +1,5 @@
|
||||||
|
[# This error is not ideal, but ok for now. It should say the permission issue instead]
|
||||||
|
error: Uncaught (in promise) TypeError: Module not found: http://localhost:4545/run/invalid_utf8.ts
|
||||||
|
await import("./branch.ts");
|
||||||
|
^
|
||||||
|
at async file:///[WILDLINE]
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"tempDir": true,
|
||||||
|
"steps": [{
|
||||||
|
"args": "compile --unstable-raw-imports --allow-import --output bin main.ts",
|
||||||
|
"output": "compile.out"
|
||||||
|
}, {
|
||||||
|
"commandName": "./bin",
|
||||||
|
"args": [],
|
||||||
|
"output": "run.out"
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// this file has a BOM
|
||||||
|
export function add(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
13
tests/specs/compile/bytes_and_text_imports/basic/compile.out
Normal file
13
tests/specs/compile/bytes_and_text_imports/basic/compile.out
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Download http://localhost:4545/run/invalid_utf8.ts
|
||||||
|
Check file:///[WILDLINE]/main.ts
|
||||||
|
Compile file:///[WILDLINE]/main.ts to [WILDLINE]
|
||||||
|
|
||||||
|
Embedded Files
|
||||||
|
|
||||||
|
[WILDLINE]
|
||||||
|
├── add_bom.ts ([WILDLINE])
|
||||||
|
├── hello_bom.txt ([WILDLINE])
|
||||||
|
├── lossy.ts ([WILDLINE])
|
||||||
|
└── main.ts ([WILDLINE])
|
||||||
|
|
||||||
|
[WILDCARD]
|
|
@ -0,0 +1 @@
|
||||||
|
hello
|
|
@ -0,0 +1 @@
|
||||||
|
console.log("This is a test: À");
|
21
tests/specs/compile/bytes_and_text_imports/basic/main.ts
Normal file
21
tests/specs/compile/bytes_and_text_imports/basic/main.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import hello from "./hello_bom.txt" with { type: "text" };
|
||||||
|
import helloBytes from "./hello_bom.txt" with { type: "bytes" };
|
||||||
|
import { add } from "./add_bom.ts";
|
||||||
|
import addBytes from "./add_bom.ts" with { type: "bytes" };
|
||||||
|
import addText from "./add_bom.ts" with { type: "text" };
|
||||||
|
import "./lossy.ts";
|
||||||
|
import invalidUtf8Bytes from "./lossy.ts" with { type: "bytes" };
|
||||||
|
import invalidUtf8Text from "./lossy.ts" with { type: "text" };
|
||||||
|
import "http://localhost:4545/run/invalid_utf8.ts";
|
||||||
|
import remoteInvalidUtf8Bytes from "http://localhost:4545/run/invalid_utf8.ts" with { type: "bytes" };
|
||||||
|
import removeInvalidUtf8Text from "http://localhost:4545/run/invalid_utf8.ts" with { type: "text" };
|
||||||
|
|
||||||
|
console.log(hello, hello.length);
|
||||||
|
console.log(helloBytes, helloBytes.length);
|
||||||
|
console.log(addText, addText.length);
|
||||||
|
console.log(addBytes, addBytes.length);
|
||||||
|
console.log(invalidUtf8Bytes, invalidUtf8Bytes.length);
|
||||||
|
console.log(invalidUtf8Text, invalidUtf8Text.length);
|
||||||
|
console.log(remoteInvalidUtf8Bytes, remoteInvalidUtf8Bytes.length);
|
||||||
|
console.log(removeInvalidUtf8Text, removeInvalidUtf8Text.length);
|
||||||
|
console.log(add(1, 2));
|
42
tests/specs/compile/bytes_and_text_imports/basic/run.out
Normal file
42
tests/specs/compile/bytes_and_text_imports/basic/run.out
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
This is a test: <20>
|
||||||
|
This is a test: <20>
|
||||||
|
hello 5
|
||||||
|
Uint8Array(8) [
|
||||||
|
239, 187, 191,
|
||||||
|
104, 101, 108,
|
||||||
|
108, 111
|
||||||
|
] 8
|
||||||
|
// this file has a BOM
|
||||||
|
export function add(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
85
|
||||||
|
Uint8Array(88) [
|
||||||
|
239, 187, 191, 47, 47, 32, 116, 104, 105, 115, 32, 102,
|
||||||
|
105, 108, 101, 32, 104, 97, 115, 32, 97, 32, 66, 79,
|
||||||
|
77, 10, 101, 120, 112, 111, 114, 116, 32, 102, 117, 110,
|
||||||
|
99, 116, 105, 111, 110, 32, 97, 100, 100, 40, 97, 58,
|
||||||
|
32, 110, 117, 109, 98, 101, 114, 44, 32, 98, 58, 32,
|
||||||
|
110, 117, 109, 98, 101, 114, 41, 32, 123, 10, 32, 32,
|
||||||
|
114, 101, 116, 117, 114, 110, 32, 97, 32, 43, 32, 98,
|
||||||
|
59, 10, 125, 10
|
||||||
|
] 88
|
||||||
|
Uint8Array(34) [
|
||||||
|
99, 111, 110, 115, 111, 108, 101, 46,
|
||||||
|
108, 111, 103, 40, 34, 84, 104, 105,
|
||||||
|
115, 32, 105, 115, 32, 97, 32, 116,
|
||||||
|
101, 115, 116, 58, 32, 192, 34, 41,
|
||||||
|
59, 10
|
||||||
|
] 34
|
||||||
|
console.log("This is a test: <20>");
|
||||||
|
34
|
||||||
|
Uint8Array(34) [
|
||||||
|
99, 111, 110, 115, 111, 108, 101, 46,
|
||||||
|
108, 111, 103, 40, 34, 84, 104, 105,
|
||||||
|
115, 32, 105, 115, 32, 97, 32, 116,
|
||||||
|
101, 115, 116, 58, 32, 192, 34, 41,
|
||||||
|
59, 10
|
||||||
|
] 34
|
||||||
|
console.log("This is a test: <20>");
|
||||||
|
34
|
||||||
|
3
|
1
tests/testdata/run/invalid_utf8.ts
vendored
Normal file
1
tests/testdata/run/invalid_utf8.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
console.log("This is a test: À");
|
Loading…
Add table
Add a link
Reference in a new issue