Skip to content

Commit

Permalink
Merge 790faa9 into f09057d
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony authored Apr 14, 2023
2 parents f09057d + 790faa9 commit 8966c65
Show file tree
Hide file tree
Showing 99 changed files with 3,500 additions and 136 deletions.
59 changes: 59 additions & 0 deletions crates/turbopack-dev/js/src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
/** @typedef {import('../types').SourceType.Update} SourceTypeUpdate */
/** @typedef {import('../types').Exports} Exports */
/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */
/** @typedef {import('../types').RequireContext} RequireContext */
/** @typedef {import('../types').RequireContextMap} RequireContextMap */

/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */
/** @typedef {import('../types/hot').Hot} Hot */
Expand Down Expand Up @@ -187,6 +189,61 @@ function commonJsRequire(sourceModule, id) {
return module.exports;
}

/**
* @param {Module} sourceModule
* @param {RequireContextMap} map
* @returns {RequireContext}
*/
function requireContext(sourceModule, map) {
/**
* @param {ModuleId} id
* @returns {Exports}
*/
function requireContext(id) {
const entry = map[id];

if (!entry) {
throw new Error(
`module ${id} is required from a require.context, but is not in the context`
);
}

return entry.internal
? commonJsRequire(sourceModule, entry.id())
: externalRequire(entry.id(), false);
}

/**
* @returns {ModuleId[]}
*/
requireContext.keys = () => {
return Object.keys(map);
};

/**
* @param {ModuleId} id
* @returns {ModuleId}
*/
requireContext.resolve = (id) => {
const entry = map[id];

if (!entry) {
throw new Error(
`module ${id} is resolved from a require.context, but is not in the context`
);
}

return entry.id();
};

return requireContext;
}

/**
* @param {ModuleId} id
* @param {boolean} esm
* @returns {Exports | EsmInteropNamespace}
*/
function externalRequire(id, esm) {
let raw;
try {
Expand Down Expand Up @@ -252,6 +309,7 @@ const SourceTypeUpdate = 2;
* @returns {Module}
*/
function instantiateModule(id, source) {
/** @type {ModuleFactory} */
const moduleFactory = moduleFactories[id];
if (typeof moduleFactory !== "function") {
// This can happen if modules incorrectly handle HMR disposes/updates,
Expand Down Expand Up @@ -312,6 +370,7 @@ function instantiateModule(id, source) {
e: module.exports,
r: commonJsRequire.bind(null, module),
x: externalRequire,
f: requireContext.bind(null, module),
i: esmImport.bind(null, module),
s: esm.bind(null, module.exports),
j: cjs.bind(null, module.exports),
Expand Down
23 changes: 22 additions & 1 deletion crates/turbopack-dev/js/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ type SourceInfo =
type ModuleCache = Record<ModuleId, Module>;

type CommonJsRequire = (moduleId: ModuleId) => Exports;
type CommonJsExport = (exports: Record<string, any>) => void;

type RequireContextFactory = (
dir: string,
useSubdirectories = true
) => RequireContext;

type RequireContextMap = Record<
ModuleId,
{ internal: boolean; id: () => ModuleId }
>;

interface RequireContext {
(moduleId: ModuleId): Exports | EsmInteropNamespace;
keys(): ModuleId[];
resolve(moduleId: ModuleId): ModuleId;
}

export type EsmInteropNamespace = Record<string, any>;
type EsmImport = (
Expand All @@ -87,13 +104,17 @@ type LoadChunk = (chunkPath: ChunkPath) => Promise<any> | undefined;
interface TurbopackContext {
e: Module["exports"];
r: CommonJsRequire;
x: NodeJS.Require;
f: RequireContextFactory;
i: EsmImport;
s: EsmExport;
j: CommonJsExport;
v: ExportValue;
m: Module;
c: ModuleCache;
l: LoadChunk;
p: Partial<NodeJS.Process> & Pick<NodeJS.Process, "env">;
g: globalThis;
__dirname: string;
}

type ModuleFactory = (
Expand Down
4 changes: 1 addition & 3 deletions crates/turbopack-dev/js/types/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { ModuleId } from "./index";
import { RuntimeBackend, TurbopackGlobals } from "types";
import { RefreshRuntimeGlobals } from "@next/react-refresh-utils/dist/runtime";
import { ModuleId, ChunkPath } from "./index";

export type ModuleEffect =
| {
Expand Down
2 changes: 2 additions & 0 deletions crates/turbopack-dev/js/types/runtime.none.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ChunkPath, ModuleId } from "./index";

export type ChunkRunner = {
requiredChunks: Set<ChunkPath>;
chunkPath: ChunkPath;
Expand Down
1 change: 1 addition & 0 deletions crates/turbopack-dev/src/ecmascript/module_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(super) async fn module_factory(content: EcmascriptChunkItemContentVc) -> Res
let mut args = vec![
"r: __turbopack_require__",
"x: __turbopack_external_require__",
"f: __turbopack_require_context__",
"i: __turbopack_import__",
"s: __turbopack_esm__",
"v: __turbopack_export_value__",
Expand Down
156 changes: 153 additions & 3 deletions crates/turbopack-ecmascript/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use std::{
sync::Arc,
};

use indexmap::IndexSet;
use indexmap::{IndexMap, IndexSet};
use num_bigint::BigInt;
use num_traits::identities::Zero;
use once_cell::sync::Lazy;
use regex::Regex;
use swc_core::{
common::Mark,
ecma::{
Expand Down Expand Up @@ -171,6 +173,14 @@ impl ConstantValue {
}
}

pub fn as_bool(&self) -> Option<bool> {
match self {
Self::True => Some(true),
Self::False => Some(false),
_ => None,
}
}

pub fn is_truthy(&self) -> bool {
match self {
Self::Undefined | Self::False | Self::Null => false,
Expand Down Expand Up @@ -1358,6 +1368,10 @@ impl JsValue {
),
WellKnownFunctionKind::Require => ("require".to_string(), "The require method from CommonJS"),
WellKnownFunctionKind::RequireResolve => ("require.resolve".to_string(), "The require.resolve method from CommonJS"),
WellKnownFunctionKind::RequireContext => ("require.context".to_string(), "The require.context method from webpack"),
WellKnownFunctionKind::RequireContextRequire(..) => ("require.context(...)".to_string(), "The require.context(...) method from webpack: https://webpack.js.org/api/module-methods/#requirecontext"),
WellKnownFunctionKind::RequireContextRequireKeys(..) => ("require.context(...).keys".to_string(), "The require.context(...).keys method from webpack: https://webpack.js.org/guides/dependency-management/#requirecontext"),
WellKnownFunctionKind::RequireContextRequireResolve(..) => ("require.context(...).resolve".to_string(), "The require.context(...).resolve method from webpack: https://webpack.js.org/guides/dependency-management/#requirecontext"),
WellKnownFunctionKind::Define => ("define".to_string(), "The define method from AMD"),
WellKnownFunctionKind::FsReadMethod(name) => (
format!("fs.{name}"),
Expand Down Expand Up @@ -1571,6 +1585,14 @@ impl JsValue {
}
}

/// Returns the constant bool if the value represents a constant boolean.
pub fn as_bool(&self) -> Option<bool> {
match self {
JsValue::Constant(c) => c.as_bool(),
_ => None,
}
}

/// Checks if the value is truthy. Returns None if we don't know. Returns
/// Some if we know if or if not the value is truthy.
pub fn is_truthy(&self) -> Option<bool> {
Expand Down Expand Up @@ -2852,6 +2874,110 @@ impl WellKnownObjectKind {
}
}

#[derive(Debug, Clone)]
pub struct RequireContextOptions {
pub dir: String,
pub include_subdirs: bool,
/// this is a regex (pattern, flags)
pub filter: Regex,
}

/// Convert an ECMAScript regex to a Rust regex.
fn regex_from_js(pattern: &str, flags: &str) -> Result<Regex, &'static str> {
// rust regex doesn't allow escaped slashes, but they are necessary in js
let pattern = pattern.replace("\\/", "/");

let mut applied_flags = String::new();
for flag in flags.chars() {
match flag {
// indices for substring matches: not relevant for the regex itself
'd' => {}
// global: default in rust, ignore
'g' => {}
// case-insensitive: letters match both upper and lower case
'i' => applied_flags.push('i'),
// multi-line mode: ^ and $ match begin/end of line
'm' => applied_flags.push('m'),
// allow . to match \n
's' => applied_flags.push('s'),
// Unicode support (enabled by default)
'u' => applied_flags.push('u'),
// sticky search: not relevant for the regex itself
'y' => {}
_ => return Err("unsupported flags in regex"),
}
}

let regex = if !applied_flags.is_empty() {
format!("(?{}){}", applied_flags, pattern)
} else {
pattern
};

Regex::new(&regex).map_err(|_| "could not convert ECMAScript regex to Rust regex")
}

/// Parse the arguments passed to a require.context invocation, validate them
/// and convert them to the appropriate rust values.
pub fn parse_require_context(args: &Vec<JsValue>) -> Result<RequireContextOptions, &'static str> {
if !(1..=3).contains(&args.len()) {
// https://linear.app/vercel/issue/WEB-910/add-support-for-requirecontexts-mode-argument
return Err("require.context() only supports 1-3 arguments (mode is not supported)");
}

let Some(dir) = args[0].as_str().map(|s| s.to_string()) else {
return Err("require.context(dir, ...) requires dir to be a constant string");
};

let include_subdirs = if let Some(include_subdirs) = args.get(1) {
if let Some(include_subdirs) = include_subdirs.as_bool() {
include_subdirs
} else {
return Err(
"require.context(..., includeSubdirs, ...) requires includeSubdirs to be a \
constant boolean",
);
}
} else {
true
};

let filter = if let Some(filter) = args.get(2) {
if let JsValue::Constant(ConstantValue::Regex(pattern, flags)) = filter {
regex_from_js(pattern, flags)?
} else {
return Err("require.context(..., ..., filter) requires filter to be a regex");
}
} else {
// https://webpack.js.org/api/module-methods/#requirecontext
// > optional, default /^\.\/.*$/, any file
static DEFAULT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\\./.*$").unwrap());

DEFAULT_REGEX.clone()
};

Ok(RequireContextOptions {
dir,
include_subdirs,
filter,
})
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequireContextValue {
pub(crate) map: IndexMap<String, String>,
}

impl Hash for RequireContextValue {
fn hash<H: Hasher>(&self, state: &mut H) {
self.map.len().hash(state);
for (k, v) in self.map.iter() {
k.hash(state);
v.hash(state);
}
}
}

/// A list of well-known functions that have special meaning in the analysis.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum WellKnownFunctionKind {
Expand All @@ -2863,6 +2989,10 @@ pub enum WellKnownFunctionKind {
Import,
Require,
RequireResolve,
RequireContext,
RequireContextRequire(RequireContextValue),
RequireContextRequireKeys(RequireContextValue),
RequireContextRequireResolve(RequireContextValue),
Define,
FsReadMethod(JsWord),
PathToFileUrl,
Expand All @@ -2889,6 +3019,7 @@ impl WellKnownFunctionKind {
Self::Import => Some(&["import"]),
Self::Require => Some(&["require"]),
Self::RequireResolve => Some(&["require", "resolve"]),
Self::RequireContext => Some(&["require", "context"]),
Self::Define => Some(&["define"]),
_ => None,
}
Expand All @@ -2904,13 +3035,14 @@ pub mod test_utils {
use std::sync::Arc;

use anyhow::Result;
use indexmap::IndexMap;
use turbopack_core::compile_time_info::CompileTimeInfoVc;

use super::{
builtin::early_replace_builtin, well_known::replace_well_known, JsValue, ModuleValue,
WellKnownFunctionKind, WellKnownObjectKind,
};
use crate::analyzer::builtin::replace_builtin;
use crate::analyzer::{builtin::replace_builtin, parse_require_context, RequireContextValue};

pub async fn early_visitor(mut v: JsValue) -> Result<(JsValue, bool)> {
let m = early_replace_builtin(&mut v);
Expand All @@ -2928,7 +3060,25 @@ pub mod test_utils {
ref args,
) => match &args[0] {
JsValue::Constant(v) => (v.to_string() + "/resolved/lib/index.js").into(),
_ => JsValue::Unknown(Some(Arc::new(v)), "resolve.resolve non constant"),
_ => JsValue::Unknown(Some(Arc::new(v)), "require.resolve non constant"),
},
JsValue::Call(
_,
box JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext),
ref args,
) => match parse_require_context(args) {
Ok(options) => {
let mut map = IndexMap::new();

map.insert("./a".into(), format!("[context: {}]/a", options.dir));
map.insert("./b".into(), format!("[context: {}]/b", options.dir));
map.insert("./c".into(), format!("[context: {}]/c", options.dir));

JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(
RequireContextValue { map },
))
}
Err(reason) => JsValue::Unknown(Some(Arc::new(v)), reason),
},
JsValue::FreeVar(var) => match &*var {
"require" => JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
Expand Down
Loading

0 comments on commit 8966c65

Please sign in to comment.