Skip to content

Commit 650b41f

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix-node-modules-test-glob
# Conflicts: # lib/utils.ts
2 parents 99713f3 + a58c605 commit 650b41f

39 files changed

Lines changed: 1414 additions & 59 deletions

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,73 @@ await build({
173173
});
174174
```
175175

176+
### Test Preload Module
177+
178+
Specify a module to load before running the tests with the `testPreloadModule`
179+
build option:
180+
181+
```ts
182+
await build({
183+
// ...etc...
184+
testPreloadModule: "./scripts/test_preload.ts",
185+
});
186+
```
187+
188+
This is useful for setting up the Node.js environment the tests run in without
189+
affecting the distributed code. For example, when the distributed code assumes a
190+
global exists that Node.js doesn't have:
191+
192+
```ts
193+
// scripts/test_preload.ts
194+
import { Headers, Response } from "npm:undici";
195+
196+
Object.assign(globalThis, { Headers, Response });
197+
```
198+
199+
The module is transformed and type checked like the test files are and it is not
200+
included in the npm package. It is loaded once for each of the emitted script
201+
and ESM output, before any test file.
202+
203+
### Polyfills
204+
205+
dnt adds polyfills for language features that may not exist in the environment
206+
implied by `compilerOptions.target`. If you're targeting a runtime that already
207+
supports a feature, use the `polyfills` build option to opt out and keep the
208+
extra code out of your package:
209+
210+
```ts
211+
await build({
212+
// ...etc...
213+
polyfills: {
214+
importMeta: false,
215+
},
216+
});
217+
```
218+
219+
Pass `true` or `false` instead of an object to enable or disable every polyfill
220+
at once. Anything you don't specify continues to be decided by the target, so
221+
`polyfills` overrides the target rather than replacing it.
222+
223+
The supported names are `arrayFindLast`, `arrayFromAsync`, `errorCause`,
224+
`importMeta`, `objectHasOwn`, `promiseWithResolvers`, and `stringReplaceAll`.
225+
226+
Note that the polyfills also provide the type declarations for the features they
227+
polyfill, so opting out of one means relying on your `compilerOptions.lib`
228+
declarations for it instead.
229+
230+
The `importMeta` polyfill can only be disabled when the `scriptModule` build
231+
option is `false`, because `import.meta` is not valid CommonJS:
232+
233+
```ts
234+
await build({
235+
// ...etc...
236+
scriptModule: false,
237+
polyfills: {
238+
importMeta: false,
239+
},
240+
});
241+
```
242+
176243
### Shims
177244

178245
dnt will shim the globals specified in the build options. For example, if you

deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"tests/node_types_project/npm",
5252
"tests/package_mappings_project/npm",
5353
"tests/polyfill_array_find_last_project/npm",
54+
"tests/polyfill_disabled_project/npm",
5455
"tests/polyfill_project/npm",
5556
"tests/shim_project/npm",
5657
"tests/test_project/npm",

lib/compiler_transforms.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,17 @@ Deno.test("transform import.meta.resolve expressions in esModule", () => {
7070
`function test(specifier) { globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).resolve(specifier); }\n`,
7171
);
7272
});
73+
74+
Deno.test("does not transform new.target in commonjs", () => {
75+
testImportReplacementsCjs(
76+
"function test(...args) { return Reflect.construct(Struct, args, new.target); }",
77+
`function test(...args) { return Reflect.construct(Struct, args, new.target); }\n`,
78+
);
79+
});
80+
81+
Deno.test("does not transform new.target in esModule", () => {
82+
testImportReplacementsEsm(
83+
"function test(...args) { return Reflect.construct(Struct, args, new.target); }",
84+
`function test(...args) { return Reflect.construct(Struct, args, new.target); }\n`,
85+
);
86+
});

lib/compiler_transforms.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ export const transformImportMeta: ts.TransformerFactory<ts.SourceFile> = (
1414
return (sourceFile) => ts.visitEachChild(sourceFile, visitNode, context);
1515

1616
function visitNode(node: ts.Node): ts.Node {
17-
// find `import.meta`
18-
if (ts.isMetaProperty(node)) {
17+
// find `import.meta` (not `new.target`, which is also a meta property)
18+
if (
19+
ts.isMetaProperty(node) &&
20+
node.keywordToken === ts.SyntaxKind.ImportKeyword
21+
) {
1922
if (isScriptModule) {
2023
return getReplacementImportMetaScript();
2124
} else {

lib/npm_ignore.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { OutputFile } from "../transform.ts";
44
import type { SourceMapOptions } from "./compiler.ts";
5+
import { toDtsFilePath, toJsFilePath } from "./utils.ts";
56

67
export function getNpmIgnoreText(options: {
78
sourceMap?: SourceMapOptions;
@@ -26,8 +27,8 @@ export function getNpmIgnoreText(options: {
2627

2728
function* getTestFileNames() {
2829
for (const file of options.testFiles) {
29-
const filePath = file.filePath.replace(/\.ts$/i, ".js");
30-
const dtsFilePath = file.filePath.replace(/\.ts$/i, ".d.ts");
30+
const filePath = toJsFilePath(file.filePath);
31+
const dtsFilePath = toDtsFilePath(file.filePath);
3132
if (options.includeEsModule) {
3233
const esmFilePath = `/esm/${filePath}`;
3334
yield esmFilePath;

lib/package_json.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { EntryPoint, ShimOptions } from "../mod.ts";
44
import type { TransformOutput } from "../transform.ts";
55
import type { PackageJson } from "./types.ts";
6-
import { getDntVersion } from "./utils.ts";
6+
import { getDntVersion, toDtsFilePath, toJsFilePath } from "./utils.ts";
77

88
export interface GetPackageJsonOptions {
99
transformOutput: TransformOutput;
@@ -32,8 +32,8 @@ export function getPackageJson({
3232
.main.entryPoints.map((e, i) => ({
3333
name: entryPoints[i].name,
3434
kind: entryPoints[i].kind ?? "export",
35-
path: e.replace(/\.tsx?$/i, ".js"),
36-
types: e.replace(/\.tsx?$/i, ".d.ts"),
35+
path: toJsFilePath(e),
36+
types: toDtsFilePath(e),
3737
}));
3838
const exports = finalEntryPoints.filter((e) => e.kind === "export");
3939
const binaries = finalEntryPoints.filter((e) => e.kind === "bin");

lib/polyfills.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2018-2024 the Deno authors. MIT license.
2+
3+
import { assertEquals, assertThrows } from "@std/assert";
4+
import {
5+
resolvePolyfillOptions,
6+
resolveUseImportMetaPolyfill,
7+
} from "./polyfills.ts";
8+
9+
Deno.test("resolvePolyfillOptions - undefined leaves everything to the target", () => {
10+
assertEquals(resolvePolyfillOptions(undefined), {});
11+
});
12+
13+
Deno.test("resolvePolyfillOptions - boolean applies to every polyfill", () => {
14+
assertEquals(resolvePolyfillOptions(false), {
15+
arrayFindLast: false,
16+
arrayFromAsync: false,
17+
errorCause: false,
18+
importMeta: false,
19+
objectHasOwn: false,
20+
promiseWithResolvers: false,
21+
stringReplaceAll: false,
22+
});
23+
assertEquals(
24+
Object.values(resolvePolyfillOptions(true)).every((v) => v),
25+
true,
26+
);
27+
});
28+
29+
Deno.test("resolvePolyfillOptions - object only includes what's specified", () => {
30+
assertEquals(
31+
resolvePolyfillOptions({ importMeta: false, errorCause: true }),
32+
{ importMeta: false, errorCause: true },
33+
);
34+
});
35+
36+
Deno.test("resolvePolyfillOptions - undefined values fall back to the target", () => {
37+
assertEquals(
38+
resolvePolyfillOptions({ importMeta: undefined, errorCause: true }),
39+
{ errorCause: true },
40+
);
41+
});
42+
43+
Deno.test("resolvePolyfillOptions - throws for an unknown polyfill", () => {
44+
assertThrows(
45+
() =>
46+
resolvePolyfillOptions(
47+
{ importMata: false } as Record<string, boolean>,
48+
),
49+
Error,
50+
"Unknown polyfill 'importMata'",
51+
);
52+
});
53+
54+
Deno.test("resolveUseImportMetaPolyfill - required when emitting a script module", () => {
55+
assertEquals(
56+
resolveUseImportMetaPolyfill({
57+
polyfills: {},
58+
target: "Latest",
59+
emitScriptModule: true,
60+
}),
61+
true,
62+
);
63+
});
64+
65+
Deno.test("resolveUseImportMetaPolyfill - throws when disabled with a script module", () => {
66+
assertThrows(
67+
() =>
68+
resolveUseImportMetaPolyfill({
69+
polyfills: { importMeta: false },
70+
target: "ES2021",
71+
emitScriptModule: true,
72+
}),
73+
Error,
74+
"cannot be disabled when emitting a script module",
75+
);
76+
});
77+
78+
Deno.test("resolveUseImportMetaPolyfill - esm only follows the target by default", () => {
79+
assertEquals(
80+
resolveUseImportMetaPolyfill({
81+
polyfills: {},
82+
target: "ES2021",
83+
emitScriptModule: false,
84+
}),
85+
true,
86+
);
87+
assertEquals(
88+
resolveUseImportMetaPolyfill({
89+
polyfills: {},
90+
target: "Latest",
91+
emitScriptModule: false,
92+
}),
93+
false,
94+
);
95+
});
96+
97+
Deno.test("resolveUseImportMetaPolyfill - esm only respects an explicit override", () => {
98+
assertEquals(
99+
resolveUseImportMetaPolyfill({
100+
polyfills: { importMeta: false },
101+
target: "ES2021",
102+
emitScriptModule: false,
103+
}),
104+
false,
105+
);
106+
assertEquals(
107+
resolveUseImportMetaPolyfill({
108+
polyfills: { importMeta: true },
109+
target: "Latest",
110+
emitScriptModule: false,
111+
}),
112+
true,
113+
);
114+
});

lib/polyfills.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2018-2024 the Deno authors. MIT license.
2+
3+
import type { PolyfillName, PolyfillOptions, ScriptTarget } from "./types.ts";
4+
5+
/** Every polyfill dnt knows how to apply. */
6+
export const polyfillNames: readonly PolyfillName[] = [
7+
"arrayFindLast",
8+
"arrayFromAsync",
9+
"errorCause",
10+
"importMeta",
11+
"objectHasOwn",
12+
"promiseWithResolvers",
13+
"stringReplaceAll",
14+
];
15+
16+
/** Resolves the user provided polyfill options into an explicit
17+
* enabled/disabled value per polyfill.
18+
*
19+
* Polyfills that the user didn't specify are left out of the result so that
20+
* the script target continues to decide whether they're used.
21+
*/
22+
export function resolvePolyfillOptions(
23+
options: PolyfillOptions | undefined,
24+
): Record<string, boolean> {
25+
if (options == null) {
26+
return {};
27+
}
28+
if (typeof options === "boolean") {
29+
return Object.fromEntries(polyfillNames.map((name) => [name, options]));
30+
}
31+
32+
const resolved: Record<string, boolean> = {};
33+
for (const [name, enabled] of Object.entries(options)) {
34+
if (enabled == null) {
35+
continue;
36+
}
37+
if (!polyfillNames.includes(name as PolyfillName)) {
38+
throw new Error(
39+
`Unknown polyfill '${name}' specified in the 'polyfills' option. ` +
40+
`Supported polyfills: ${polyfillNames.join(", ")}`,
41+
);
42+
}
43+
resolved[name] = enabled;
44+
}
45+
return resolved;
46+
}
47+
48+
/** Whether the `import.meta` polyfill applies, given the resolved polyfill
49+
* overrides and the rest of the build options.
50+
*
51+
* `import.meta` is a syntax error in CommonJS, so the polyfill is required
52+
* whenever a script module is emitted regardless of the target.
53+
*/
54+
export function resolveUseImportMetaPolyfill(options: {
55+
polyfills: Record<string, boolean>;
56+
target: ScriptTarget;
57+
emitScriptModule: boolean;
58+
}): boolean {
59+
const explicit = options.polyfills["importMeta"];
60+
if (options.emitScriptModule) {
61+
if (explicit === false) {
62+
throw new Error(
63+
"The 'importMeta' polyfill cannot be disabled when emitting a script " +
64+
"module because `import.meta` is not valid CommonJS. Set the " +
65+
"'scriptModule' build option to false to distribute an ES module only.",
66+
);
67+
}
68+
return true;
69+
}
70+
return explicit ?? options.target !== "Latest";
71+
}

0 commit comments

Comments
 (0)