mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-13 09:07:29 +02:00
feat(core): support TypeScript + ESM configuration (#9317)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
336a44f3ea
commit
45f1a669b5
126 changed files with 2054 additions and 914 deletions
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs
generated
Normal file
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
exports.someNamedExport = 42;
|
||||
|
||||
module.exports = {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
};
|
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs.js
generated
Normal file
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs.js
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
exports.someNamedExport = 42;
|
||||
|
||||
module.exports = {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
};
|
11
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs.ts
generated
Normal file
11
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.cjs.ts
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
exports.someNamedExport = 42 as number;
|
||||
|
||||
module.exports = {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
} satisfies {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthYear: number;
|
||||
};
|
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.esm.js
generated
Normal file
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.esm.js
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const someNamedExport = 42;
|
||||
|
||||
export default {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
};
|
11
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.esm.ts
generated
Normal file
11
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.esm.ts
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const someNamedExport: number = 42;
|
||||
|
||||
export default {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
} satisfies {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthYear: number;
|
||||
};
|
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.mjs
generated
Normal file
7
packages/docusaurus-utils/src/__tests__/__fixtures__/moduleUtils/user/user.mjs
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const someNamedExport = 42;
|
||||
|
||||
export default {
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
birthYear: 1986,
|
||||
};
|
289
packages/docusaurus-utils/src/__tests__/moduleUtils.test.ts
Normal file
289
packages/docusaurus-utils/src/__tests__/moduleUtils.test.ts
Normal file
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import tmp from 'tmp-promise';
|
||||
import dedent from 'dedent';
|
||||
import {loadFreshModule} from '../moduleUtils';
|
||||
|
||||
async function createTmpDir() {
|
||||
return (
|
||||
await tmp.dir({
|
||||
prefix: 'jest-tmp-moduleUtils-tests',
|
||||
})
|
||||
).path;
|
||||
}
|
||||
|
||||
async function moduleGraphHelpers() {
|
||||
const dir = await createTmpDir();
|
||||
|
||||
async function fileHelper(name: string, initialContent?: string) {
|
||||
const filePath = path.resolve(dir, name);
|
||||
if (initialContent) {
|
||||
await fs.outputFile(filePath, initialContent);
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
write: (content: string) => fs.outputFile(filePath, content),
|
||||
load: () => loadFreshModule(filePath),
|
||||
};
|
||||
}
|
||||
|
||||
return {fileHelper};
|
||||
}
|
||||
|
||||
async function loadFixtureModule(fixtureName: string) {
|
||||
return loadFreshModule(
|
||||
path.resolve(__dirname, '__fixtures__/moduleUtils', fixtureName),
|
||||
);
|
||||
}
|
||||
|
||||
describe('loadFreshModule', () => {
|
||||
describe('can load CJS user module', () => {
|
||||
async function testUserFixture(fixtureName: string) {
|
||||
const userFixturePath = `user/${fixtureName}`;
|
||||
const userModule = await loadFixtureModule(userFixturePath);
|
||||
expect(userModule).toEqual({
|
||||
birthYear: 1986,
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
});
|
||||
}
|
||||
|
||||
it('for .cjs.js', async () => {
|
||||
await testUserFixture('user.cjs.js');
|
||||
});
|
||||
|
||||
it('for .cjs.ts', async () => {
|
||||
await testUserFixture('user.cjs.ts');
|
||||
});
|
||||
|
||||
it('for .cjs', async () => {
|
||||
await testUserFixture('user.cjs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('can load ESM user module', () => {
|
||||
async function testUserFixture(fixtureName: string) {
|
||||
const userFixturePath = `user/${fixtureName}`;
|
||||
const userModule = await loadFixtureModule(userFixturePath);
|
||||
expect(userModule).toEqual({
|
||||
birthYear: 1986,
|
||||
firstName: 'Sebastien',
|
||||
lastName: 'Lorber',
|
||||
someNamedExport: 42,
|
||||
});
|
||||
}
|
||||
|
||||
it('for .esm.js', async () => {
|
||||
await testUserFixture('user.esm.js');
|
||||
});
|
||||
|
||||
it('for .esm.ts', async () => {
|
||||
await testUserFixture('user.esm.ts');
|
||||
});
|
||||
|
||||
it('for .mjs', async () => {
|
||||
await testUserFixture('user.mjs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('module graph', () => {
|
||||
it('can load and reload fresh module graph', async () => {
|
||||
const {fileHelper} = await moduleGraphHelpers();
|
||||
|
||||
const dependency1 = await fileHelper(
|
||||
'dependency1.js',
|
||||
/* language=js */
|
||||
dedent`
|
||||
export const dep1Export = "dep1 val1";
|
||||
|
||||
export default {dep1Val: "dep1 val2"}
|
||||
`,
|
||||
);
|
||||
|
||||
const dependency2 = await fileHelper(
|
||||
'dependency2.ts',
|
||||
/* language=ts */
|
||||
dedent`
|
||||
export default {dep2Val: "dep2 val"} satisfies {dep2Val: string}
|
||||
`,
|
||||
);
|
||||
|
||||
const entryFile = await fileHelper(
|
||||
'entry.js',
|
||||
/* language=js */
|
||||
dedent`
|
||||
import dependency1 from "./dependency1";
|
||||
import dependency2 from "./dependency2";
|
||||
|
||||
export default {
|
||||
someEntryValue: "entryVal",
|
||||
dependency1,
|
||||
dependency2
|
||||
};
|
||||
`,
|
||||
);
|
||||
|
||||
// Should be able to read the initial module graph
|
||||
await expect(entryFile.load()).resolves.toEqual({
|
||||
someEntryValue: 'entryVal',
|
||||
dependency1: {
|
||||
dep1Export: 'dep1 val1',
|
||||
dep1Val: 'dep1 val2',
|
||||
},
|
||||
dependency2: {
|
||||
dep2Val: 'dep2 val',
|
||||
},
|
||||
});
|
||||
await expect(dependency1.load()).resolves.toEqual({
|
||||
dep1Export: 'dep1 val1',
|
||||
dep1Val: 'dep1 val2',
|
||||
});
|
||||
await expect(dependency2.load()).resolves.toEqual({
|
||||
dep2Val: 'dep2 val',
|
||||
});
|
||||
|
||||
// Should be able to read the module graph again after updates
|
||||
await dependency1.write(
|
||||
/* language=js */
|
||||
dedent`
|
||||
export const dep1Export = "dep1 val1 updated";
|
||||
|
||||
export default {dep1Val: "dep1 val2 updated"}
|
||||
`,
|
||||
);
|
||||
await expect(entryFile.load()).resolves.toEqual({
|
||||
someEntryValue: 'entryVal',
|
||||
dependency1: {
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
},
|
||||
dependency2: {
|
||||
dep2Val: 'dep2 val',
|
||||
},
|
||||
});
|
||||
await expect(dependency1.load()).resolves.toEqual({
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
});
|
||||
await expect(dependency2.load()).resolves.toEqual({
|
||||
dep2Val: 'dep2 val',
|
||||
});
|
||||
|
||||
// Should be able to read the module graph again after updates
|
||||
await dependency2.write(
|
||||
/* language=ts */
|
||||
dedent`
|
||||
export default {dep2Val: "dep2 val updated"} satisfies {dep2Val: string}
|
||||
`,
|
||||
);
|
||||
await expect(entryFile.load()).resolves.toEqual({
|
||||
someEntryValue: 'entryVal',
|
||||
dependency1: {
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
},
|
||||
dependency2: {
|
||||
dep2Val: 'dep2 val updated',
|
||||
},
|
||||
});
|
||||
await expect(dependency1.load()).resolves.toEqual({
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
});
|
||||
await expect(dependency2.load()).resolves.toEqual({
|
||||
dep2Val: 'dep2 val updated',
|
||||
});
|
||||
|
||||
// Should be able to read the module graph again after updates
|
||||
await entryFile.write(
|
||||
/* language=js */
|
||||
dedent`
|
||||
import dependency1 from "./dependency1";
|
||||
import dependency2 from "./dependency2";
|
||||
|
||||
export default {
|
||||
someEntryValue: "entryVal updated",
|
||||
dependency1,
|
||||
dependency2,
|
||||
newAttribute: "is there"
|
||||
}
|
||||
`,
|
||||
);
|
||||
await expect(entryFile.load()).resolves.toEqual({
|
||||
someEntryValue: 'entryVal updated',
|
||||
newAttribute: 'is there',
|
||||
dependency1: {
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
},
|
||||
dependency2: {
|
||||
dep2Val: 'dep2 val updated',
|
||||
},
|
||||
});
|
||||
await expect(dependency1.load()).resolves.toEqual({
|
||||
dep1Export: 'dep1 val1 updated',
|
||||
dep1Val: 'dep1 val2 updated',
|
||||
});
|
||||
await expect(dependency2.load()).resolves.toEqual({
|
||||
dep2Val: 'dep2 val updated',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid module path param', () => {
|
||||
it('throws if module path does not exist', async () => {
|
||||
await expect(() => loadFreshModule('/some/unknown/module/path.js'))
|
||||
.rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Docusaurus could not load module at path "/some/unknown/module/path.js"
|
||||
Cause: Cannot find module '/some/unknown/module/path.js' from 'packages/docusaurus-utils/src/moduleUtils.ts'"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if module path is undefined', async () => {
|
||||
await expect(() =>
|
||||
// @ts-expect-error: undefined is invalid
|
||||
loadFreshModule(undefined),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Docusaurus could not load module at path "undefined"
|
||||
Cause: Invalid module path of type undefined"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if module path is null', async () => {
|
||||
await expect(() =>
|
||||
// @ts-expect-error: null is invalid
|
||||
loadFreshModule(null),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Docusaurus could not load module at path "null"
|
||||
Cause: Invalid module path of type null"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if module path is number', async () => {
|
||||
await expect(() =>
|
||||
// @ts-expect-error: number is invalid
|
||||
loadFreshModule(42),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Docusaurus could not load module at path "42"
|
||||
Cause: Invalid module path of type 42"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if module path is object', async () => {
|
||||
await expect(() =>
|
||||
// @ts-expect-error: object is invalid
|
||||
loadFreshModule({}),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Docusaurus could not load module at path "[object Object]"
|
||||
Cause: Invalid module path of type [object Object]"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -100,6 +100,7 @@ export {
|
|||
} from './globUtils';
|
||||
export {getFileLoaderUtils} from './webpackUtils';
|
||||
export {escapeShellArg} from './shellUtils';
|
||||
export {loadFreshModule} from './moduleUtils';
|
||||
export {
|
||||
getDataFilePath,
|
||||
getDataFileData,
|
||||
|
|
43
packages/docusaurus-utils/src/moduleUtils.ts
Normal file
43
packages/docusaurus-utils/src/moduleUtils.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import jiti from 'jiti';
|
||||
import logger from '@docusaurus/logger';
|
||||
|
||||
/*
|
||||
jiti is able to load ESM, CJS, JSON, TS modules
|
||||
*/
|
||||
export async function loadFreshModule(modulePath: string): Promise<unknown> {
|
||||
try {
|
||||
if (typeof modulePath !== 'string') {
|
||||
throw new Error(
|
||||
logger.interpolate`Invalid module path of type name=${modulePath}`,
|
||||
);
|
||||
}
|
||||
const load = jiti(__filename, {
|
||||
// Transpilation cache, can be safely enabled
|
||||
cache: true,
|
||||
// Bypass Node.js runtime require cache
|
||||
// Same as "import-fresh" package we used previously
|
||||
requireCache: false,
|
||||
// Only take into consideration the default export
|
||||
// For now we don't need named exports
|
||||
// This also helps normalize return value for both CJS/ESM/TS modules
|
||||
interopDefault: true,
|
||||
// debug: true,
|
||||
});
|
||||
|
||||
return load(modulePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
logger.interpolate`Docusaurus could not load module at path path=${modulePath}\nCause: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
{cause: error},
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue