有些包把东西放在子路径下(es-toolkit 的 toString 只在 compat 里,主入口没有),
而摊平之后原包的 exports 映射就没了,import "es-toolkit/compat" 解析不到。
现在直接写子路径就行:
esm.Install(ctx, "es-toolkit/compat", "app/node_modules")
拉的是根包,摊平的是子路径,落到 node_modules/es-toolkit/compat/。根包和子路径
可以共存——子路径目录嵌在根包目录里,而最小 package.json 不写 exports,
所以解析器认得出来。
npm.SplitPath 负责拆名字,scoped 包名自带一个斜杠所以前两段才是包名。
262 lines
7.1 KiB
Go
262 lines
7.1 KiB
Go
package esm_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.fsdpf.net/go/jscriptx"
|
|
"git.fsdpf.net/go/jscriptx/esm"
|
|
)
|
|
|
|
// writePkg 在 dir/node_modules/name 下摆一个包。
|
|
func writePkg(t *testing.T, dir, name, version, entry, code string, deps map[string]string) {
|
|
t.Helper()
|
|
|
|
p := filepath.Join(dir, "node_modules", filepath.FromSlash(name))
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pkg := map[string]any{"name": name, "version": version, "type": "module", "main": entry}
|
|
if len(deps) > 0 {
|
|
pkg["dependencies"] = deps
|
|
}
|
|
b, _ := json.Marshal(pkg)
|
|
if err := os.WriteFile(filepath.Join(p, "package.json"), b, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(p, entry), []byte(code), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// 一棵装好的依赖树:主包 + 它依赖的包,两个都在 node_modules 顶层(npm 的平铺布局)。
|
|
func installedTree(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
|
|
writePkg(t, dir, "dep-lib", "2.0.0", "index.js",
|
|
`export function helper(x) { return "dep:" + x }`, nil)
|
|
|
|
writePkg(t, dir, "main-lib", "1.0.0", "index.js", `
|
|
import { helper } from "dep-lib"
|
|
export function greet(x) { return helper(x) + "!" }
|
|
export default { greet }
|
|
`, map[string]string{"dep-lib": "^2.0.0"})
|
|
|
|
return dir
|
|
}
|
|
|
|
// 摊平之后,产物里不能再有对依赖的 import——依赖得被内联进去。
|
|
func TestVendor_依赖被打进单文件(t *testing.T) {
|
|
from := installedTree(t)
|
|
out := t.TempDir()
|
|
|
|
res, err := esm.Vendor(from, "main-lib", out)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Version != "1.0.0" {
|
|
t.Errorf("版本 = %q", res.Version)
|
|
}
|
|
// 转发入口 + main-lib + dep-lib
|
|
if res.Modules < 2 {
|
|
t.Errorf("只打进 %d 个文件,依赖没被拉进来?", res.Modules)
|
|
}
|
|
|
|
b, err := os.ReadFile(filepath.Join(res.Dir, "index.mjs"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src := string(b)
|
|
if strings.Contains(src, `"dep-lib"`) || strings.Contains(src, `'dep-lib'`) {
|
|
t.Errorf("产物里还留着对 dep-lib 的引用,没真正内联:\n%s", src)
|
|
}
|
|
if !strings.Contains(src, "dep:") {
|
|
t.Errorf("产物里没有依赖的代码:\n%s", src)
|
|
}
|
|
}
|
|
|
|
// 摊平出来的东西要能直接被 Loader 用,脚本的 import 写法不变。
|
|
func TestVendor_摊平后脚本能直接用(t *testing.T) {
|
|
from := installedTree(t)
|
|
|
|
app := t.TempDir()
|
|
if _, err := esm.Vendor(from, "main-lib", filepath.Join(app, "node_modules")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(app, "Http"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 具名导出和默认导出都试一下
|
|
if err := os.WriteFile(filepath.Join(app, "Http", "C.ts"), []byte(`
|
|
import lib, { greet } from "main-lib"
|
|
export default class C {
|
|
Run(): string { return greet("a") + "/" + lib.greet("b") }
|
|
}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
loader, err := esm.NewLoader(app)
|
|
if err != nil {
|
|
t.Fatalf("打包失败: %v", err)
|
|
}
|
|
e, err := jscriptx.New(jscriptx.WithLoader(loader), jscriptx.WithLogger(nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
|
|
s, err := e.Script("Http/C")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
obj, err := s.New(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer obj.Close()
|
|
|
|
got, err := obj.Call(ctx, "Run")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "dep:a!/dep:b!" {
|
|
t.Errorf("Run = %#v", got)
|
|
}
|
|
}
|
|
|
|
// 只有具名导出的包(ESM 原生的很多是这样)也要能摊平。
|
|
//
|
|
// 转发源码里写 `export { default }` 会让 esbuild 报错,Vendor 得能退回去重打。
|
|
func TestVendor_没有默认导出也能摊平(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writePkg(t, dir, "named-only", "1.0.0", "index.js",
|
|
`export const answer = 42`, nil)
|
|
|
|
res, err := esm.Vendor(dir, "named-only", t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("没有默认导出就摊不平了: %v", err)
|
|
}
|
|
b, _ := os.ReadFile(filepath.Join(res.Dir, "index.mjs"))
|
|
if !strings.Contains(string(b), "42") {
|
|
t.Errorf("产物不对: %s", b)
|
|
}
|
|
}
|
|
|
|
// 包没装就要给一句说得清的错,而不是一个 file not found。
|
|
func TestVendor_包没装时说清楚该怎么办(t *testing.T) {
|
|
_, err := esm.Vendor(t.TempDir(), "not-installed", t.TempDir())
|
|
if err == nil {
|
|
t.Fatal("该报错")
|
|
}
|
|
for _, want := range []string{"not-installed", "npm i"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("错误里该提到 %q: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 写出来的 package.json 是给解析器找入口用的,少了它整个就不成立。
|
|
func TestVendor_写出最小的package_json(t *testing.T) {
|
|
res, err := esm.Vendor(installedTree(t), "main-lib", t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b, err := os.ReadFile(filepath.Join(res.Dir, "package.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pkg map[string]any
|
|
if err := json.Unmarshal(b, &pkg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for k, want := range map[string]any{
|
|
"name": "main-lib", "version": "1.0.0",
|
|
"module": "index.mjs", "main": "index.mjs",
|
|
} {
|
|
if pkg[k] != want {
|
|
t.Errorf("package.json[%s] = %#v, want %#v", k, pkg[k], want)
|
|
}
|
|
}
|
|
// 有 exports 的话解析器只认里面列的子路径,而摊平后子路径已经不存在了
|
|
if _, ok := pkg["exports"]; ok {
|
|
t.Error("不该写 exports 字段")
|
|
}
|
|
}
|
|
|
|
// 带子路径的名字要能摊平:有些包把东西放在子路径下(es-toolkit/compat),
|
|
// 而摊平之后原包的 exports 映射就没了,子路径本来解析不到。
|
|
func TestVendor_子路径当成独立的包摊平(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// 摆一个带子目录的包:根入口和 sub 入口导出不同的东西
|
|
writePkg(t, dir, "multi", "1.0.0", "index.js", `export const root = "根"`, nil)
|
|
subDir := filepath.Join(dir, "node_modules", "multi", "sub")
|
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(subDir, "index.js"),
|
|
[]byte(`export const only = "只在子路径里"`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
app := t.TempDir()
|
|
out := filepath.Join(app, "node_modules")
|
|
if _, err := esm.Vendor(dir, "multi/sub", out); err != nil {
|
|
t.Fatalf("子路径摊不平: %v", err)
|
|
}
|
|
// 根包也摊一份,验证两者能共存(子路径目录嵌在根包目录里)
|
|
if _, err := esm.Vendor(dir, "multi", out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Join(app, "Http"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(app, "Http", "C.ts"), []byte(`
|
|
import { root } from "multi"
|
|
import { only } from "multi/sub"
|
|
export default class C {
|
|
Run(): string { return root + "/" + only }
|
|
}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
loader, err := esm.NewLoader(app)
|
|
if err != nil {
|
|
t.Fatalf("打包失败: %v", err)
|
|
}
|
|
e, err := jscriptx.New(jscriptx.WithLoader(loader), jscriptx.WithLogger(nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
|
|
s, err := e.Script("Http/C")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
obj, err := s.New(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer obj.Close()
|
|
|
|
got, err := obj.Call(ctx, "Run")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "根/只在子路径里" {
|
|
t.Errorf("Run = %#v", got)
|
|
}
|
|
}
|