Skip to content

Commit

Permalink
Fixed the default export shape in strict ESM environments (#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed May 1, 2023
1 parent bb10c5d commit 93106e3
Show file tree
Hide file tree
Showing 14 changed files with 1,791 additions and 110 deletions.
17 changes: 17 additions & 0 deletions .changeset/brown-shoes-taste.md
@@ -0,0 +1,17 @@
---
"@preconstruct/cli": minor
---

Added a new `exports.importConditionDefaultExport` config option. It allows you to generate `import` exports condition (and corresponding files) to fix the export shape incompatibility between node and bundlers.

With this option set to `"default"` this will always resolve to what has been written as a default export:

```ts
// lib/src/index.js
export default 42;
export const named = "awesome";

// app/consume.mjs
import smth from "lib";
smth; // 42, and not `{ default: 42, named: 'awesome' }`
```
128 changes: 128 additions & 0 deletions packages/cli/src/__tests__/dev.ts
Expand Up @@ -417,3 +417,131 @@ test(".d.ts file with default export", async () => {
`);
});

test("with default", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@mjs-proxy/repo",
preconstruct: {
packages: ["packages/pkg-a"],
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
exports: {
".": {
module: "./dist/pkg-a.esm.js",
import: "./dist/pkg-a.cjs.mjs",
default: "./dist/pkg-a.cjs.js",
},
"./something": {
module: "./something/dist/pkg-a-something.esm.js",
import: "./something/dist/pkg-a-something.cjs.mjs",
default: "./something/dist/pkg-a-something.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
entrypoints: ["index.ts", "something.ts"],
exports: {
importConditionDefaultExport: "default",
},
},
}),
"packages/pkg-a/something/package.json": JSON.stringify({
main: "dist/pkg-a-something.cjs.js",
module: "dist/pkg-a-something.esm.js",
}),
"packages/pkg-a/src/index.ts": ts`
export const thing = "index";
export default true;
`,
"packages/pkg-a/src/something.ts": ts`
export const something = "something";
export default 100;
`,
"packages/pkg-a/not-exported.ts": ts`
export const notExported = true;
export default "foo";
`,

"packages/pkg-a/node_modules": {
kind: "symlink",
path: repoNodeModules,
},
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "NodeNext",
moduleResolution: "nodenext",
strict: true,
declaration: true,
},
}),
});
await fs.ensureSymlink(
path.join(dir, "packages/pkg-a"),
path.join(dir, "node_modules/pkg-a")
);
await dev(dir);

expect(
await getFiles(dir, [
"packages/**/dist/**",
"!packages/**/dist/*.cjs.js",
"!**/node_modules",
])
).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.mts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "../src/index.js";
import ns from "../src/index.js";
export default ns.default;
//# sourceMappingURL=pkg-a.cjs.d.mts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.mts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.mts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA"}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "../src/index";
export { default } from "../src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA"}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.mjs ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./pkg-a.cjs.js";
import ns from "./pkg-a.cjs.js";
export default ns.default;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.esm.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export const thing = "index";
export default true;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.cjs.d.mts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "../../src/something.js";
import ns from "../../src/something.js";
export default ns.default;
//# sourceMappingURL=pkg-a-something.cjs.d.mts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.cjs.d.mts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a-something.cjs.d.mts","sourceRoot":"","sources":["../../src/something.ts"],"names":[],"mappings":"AAAA"}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "../../src/something";
export { default } from "../../src/something";
//# sourceMappingURL=pkg-a-something.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a-something.cjs.d.ts","sourceRoot":"","sources":["../../src/something.ts"],"names":[],"mappings":"AAAA"}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.cjs.mjs ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./pkg-a-something.cjs.js";
import ns from "./pkg-a-something.cjs.js";
export default ns.default;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/something/dist/pkg-a-something.esm.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export const something = "something";
export default 100;
`);
});
162 changes: 159 additions & 3 deletions packages/cli/src/__tests__/validate.ts
Expand Up @@ -684,13 +684,13 @@ describe("exports field config", () => {
test("null", async () => {
const tmpPath = await exportsFieldConfigTestDir(null);
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports" field must be a boolean or an object at the package level]`
`[Error: the "preconstruct.exports" field must be a boolean or an object]`
);
});
test("some string", async () => {
const tmpPath = await exportsFieldConfigTestDir("blah");
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports" field must be a boolean or an object at the package level]`
`[Error: the "preconstruct.exports" field must be a boolean or an object]`
);
});
test("extra not object", async () => {
Expand Down Expand Up @@ -729,6 +729,14 @@ describe("exports field config", () => {
`[Error: the "preconstruct.exports" field contains an unknown key "something"]`
);
});
test("invalid importConditionDefaultExport", async () => {
const tmpPath = await exportsFieldConfigTestDir({
importConditionDefaultExport: "something",
});
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports.importConditionDefaultExport" field must be set to "default" or "namespace" if it is present]`
);
});
});

describe("true", () => {
Expand All @@ -737,7 +745,14 @@ describe("exports field config", () => {
{ envConditions: [] },
{ envConditions: [], extra: {} },
{ extra: {} },
{},
{ envConditions: [], importConditionDefaultExport: "namespace" },
{
envConditions: [],
extra: {},
importConditionDefaultExport: "namespace",
},
{ extra: {}, importConditionDefaultExport: "namespace" },
{ importConditionDefaultExport: "namespace" },
true,
];
for (const config of configsEquivalentToTrue) {
Expand Down Expand Up @@ -766,6 +781,147 @@ describe("exports field config", () => {
});
}
});
test('{ "importConditionDefaultExport": "default" }', async () => {
const tmpPath = await testdir({
"package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
exports: {
".": {
module: "./dist/pkg-a.esm.js",
import: "./dist/pkg-a.cjs.mjs",
default: "./dist/pkg-a.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
exports: {
importConditionDefaultExport: "default",
},
},
}),
"src/index.js": "",
});
await validate(tmpPath);
});
});

describe("project level exports field config", () => {
const exportsFieldConfigTestDir = (config: JSONValue) => {
return testdir({
"package.json": JSON.stringify({
name: "repo",
preconstruct: {
exports: config,
packages: ["packages/*"],
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
exports: {
".": {
module: "./dist/pkg-a.esm.js",
default: "./dist/pkg-a.cjs.js",
},
"./package.json": "./package.json",
},
}),
"packages/pkg-a/src/index.js": "",
});
};

describe("invalid", () => {
test("null", async () => {
const tmpPath = await exportsFieldConfigTestDir(null);
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports" field must be a boolean or an object]`
);
});
test("some string", async () => {
const tmpPath = await exportsFieldConfigTestDir("blah");
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports" field must be a boolean or an object]`
);
});
test("extra", async () => {
const tmpPath = await exportsFieldConfigTestDir({
extra: {
"./blah": "./blah.js",
},
});
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports.extra" field can only be configured at the package level]`
);
});
test("envConditions", async () => {
const tmpPath = await exportsFieldConfigTestDir({
envConditions: ["browser"],
});
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports.envConditions" field can only be configured at the package level]`
);
});
test("unknown key", async () => {
const tmpPath = await exportsFieldConfigTestDir({
something: true,
});
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports" field contains an unknown key "something"]`
);
});
test("invalid importConditionDefaultExport", async () => {
const tmpPath = await exportsFieldConfigTestDir({
importConditionDefaultExport: "something",
});
await expect(validate(tmpPath)).rejects.toMatchInlineSnapshot(
`[Error: the "preconstruct.exports.importConditionDefaultExport" field must be set to "default" or "namespace" if it is present]`
);
});
});
describe("true", () => {
const configsEquivalentToTrue = [
{},
{ importConditionDefaultExport: "namespace" },
true,
];
for (const config of configsEquivalentToTrue) {
test(`${JSON.stringify(config)}`, async () => {
const tmpPath = await exportsFieldConfigTestDir(config);
await validate(tmpPath);
});
}
});
test('{ "importConditionDefaultExport": "default" }', async () => {
const tmpPath = await testdir({
"package.json": JSON.stringify({
name: "repo",
preconstruct: {
exports: {
importConditionDefaultExport: "default",
},
packages: ["packages/*"],
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
exports: {
".": {
module: "./dist/pkg-a.esm.js",
import: "./dist/pkg-a.cjs.mjs",
default: "./dist/pkg-a.cjs.js",
},
"./package.json": "./package.json",
},
}),
"packages/pkg-a/src/index.js": "",
});
await validate(tmpPath);
});
});

test("no module field with exports field", async () => {
Expand Down

0 comments on commit 93106e3

Please sign in to comment.