Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support importing json modules #336

Merged
merged 9 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export const TS_EXTENSIONS = /\.([cm]ts|[tj]sx?)$/;

export const DTS_EXTENSIONS = /\.d\.(c|m)?tsx?$/;

export const JSON_EXTENSIONS = /\.json$/;

const SUPPORTED_EXTENSIONS = /((\.d)?\.(c|m)?(t|j)sx?|\.json)$/;

export function trimExtension(path: string) {
return path.replace(/((\.d)?\.(c|m)?(t|j)sx?)$/, "")
return path.replace(SUPPORTED_EXTENSIONS, "")
}

export function getDeclarationId(path: string) {
return path.replace(SUPPORTED_EXTENSIONS, ".d.ts")
}
19 changes: 12 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as path from "node:path";
import type { PluginImpl, Plugin } from "rollup";
import ts from "typescript";
import { type Options, resolveDefaultOptions, type ResolvedOptions } from "./options.js";
import { createProgram, createPrograms, dts, DTS_EXTENSIONS, formatHost, getCompilerOptions } from "./program.js";
import { createProgram, createPrograms, formatHost, getCompilerOptions } from "./program.js";
import { transform } from "./transform/index.js";
import { trimExtension } from "./helpers.js";
import { trimExtension, DTS_EXTENSIONS, JSON_EXTENSIONS, getDeclarationId } from "./helpers.js";

export type { Options };

Expand Down Expand Up @@ -119,7 +119,7 @@ const plugin: PluginImpl<Options> = (options = {}) => {
},

transform(code, id) {
if (!TS_EXTENSIONS.test(id)) {
if (!TS_EXTENSIONS.test(id) && !JSON_EXTENSIONS.test(id)) {
return null;
}

Expand All @@ -144,7 +144,7 @@ const plugin: PluginImpl<Options> = (options = {}) => {
};

const treatTsAsDts = () => {
const declarationId = id.replace(TS_EXTENSIONS, dts);
const declarationId = getDeclarationId(id);
const module = getModule(ctx, declarationId, code);
if (module) {
watchFiles(module);
Expand All @@ -153,12 +153,12 @@ const plugin: PluginImpl<Options> = (options = {}) => {
return null;
};

const generateDtsFromTs = () => {
const generateDts = () => {
const module = getModule(ctx, id, code);
if (!module || !module.source || !module.program) return null;
watchFiles(module);

const declarationId = id.replace(TS_EXTENSIONS, dts);
const declarationId = getDeclarationId(id);

let generated!: ReturnType<typeof transformPlugin.transform>;
const { emitSkipped, diagnostics } = module.program.emit(
Expand All @@ -185,8 +185,13 @@ const plugin: PluginImpl<Options> = (options = {}) => {
// if it's a .d.ts file, handle it as-is
if (DTS_EXTENSIONS.test(id)) return handleDtsFile();

// if it's a json file, use the typescript compiler to generate the declarations,
// requires `compilerOptions.resolveJsonModule: true`.
// This is also commonly used with `@rollup/plugin-json` to import JSON files.
if (JSON_EXTENSIONS.test(id)) return generateDts();

// first attempt to treat .ts files as .d.ts files, and otherwise use the typescript compiler to generate the declarations
return treatTsAsDts() ?? generateDtsFromTs();
return treatTsAsDts() ?? generateDts();
},

resolveId(source, importer) {
Expand Down
6 changes: 3 additions & 3 deletions src/program.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as path from "node:path";
import ts from "typescript";

export const DTS_EXTENSIONS = /\.d\.(c|m)?tsx?$/;
export const dts = ".d.ts";
import { DTS_EXTENSIONS } from "./helpers.js";

export const formatHost: ts.FormatDiagnosticsHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
Expand All @@ -26,6 +24,8 @@ const DEFAULT_OPTIONS: ts.CompilerOptions = {
preserveSymlinks: true,
// Ensure we can parse the latest code
target: ts.ScriptTarget.ESNext,
// Allows importing `*.json`
resolveJsonModule: true,
};

const configByPath = new Map<string, ts.ParsedCommandLine>();
Expand Down
5 changes: 3 additions & 2 deletions src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { preProcess } from "./preprocess.js";
import { convert } from "./Transformer.js";
import { ExportsFixer } from "./ExportsFixer.js";
import { TypeOnlyFixer } from "./TypeOnlyFixer.js";
import { trimExtension } from "../helpers.js";
import { JSON_EXTENSIONS, trimExtension } from "../helpers.js";

function parse(fileName: string, code: string): ts.SourceFile {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
Expand Down Expand Up @@ -81,9 +81,10 @@ export const transform = () => {
const moduleIds = this.getModuleIds()
const moduleId = Array.from(moduleIds).find((id) => trimExtension(id) === name)
const isEntry = Boolean(moduleId && this.getModuleInfo(moduleId)?.isEntry)
const isJSON = Boolean(moduleId && JSON_EXTENSIONS.test(moduleId))

let sourceFile = parse(fileName, code);
const preprocessed = preProcess({ sourceFile, isEntry });
const preprocessed = preProcess({ sourceFile, isEntry, isJSON });
// `sourceFile.fileName` here uses forward slashes
allTypeReferences.set(sourceFile.fileName, preprocessed.typeReferences);
allFileReferences.set(sourceFile.fileName, preprocessed.fileReferences);
Expand Down
20 changes: 19 additions & 1 deletion src/transform/preprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Range = [start: number, end: number];
interface PreProcessInput {
sourceFile: ts.SourceFile;
isEntry: boolean;
isJSON?: boolean;
}

interface PreProcessOutput {
Expand All @@ -39,7 +40,7 @@ interface PreProcessOutput {
* - [ ] Duplicate the identifiers of a namespace `export`, so that renaming does
* not break it
*/
export function preProcess({ sourceFile, isEntry }: PreProcessInput): PreProcessOutput {
export function preProcess({ sourceFile, isEntry, isJSON }: PreProcessInput): PreProcessOutput {
const code = new MagicString(sourceFile.getFullText());

// Only treat as global module if it's not an entry point,
Expand Down Expand Up @@ -235,6 +236,23 @@ export function preProcess({ sourceFile, isEntry }: PreProcessInput): PreProcess
if (exportedNames.size) {
code.append(`\nexport { ${[...exportedNames].join(", ")} };\n`);
}
if(isJSON && exportedNames.size) {
/**
* Add default export for JSON modules.
*
* The typescript compiler only generate named exports for each top-level key,
* but we also need a default export for JSON modules in most cases.
* This also aligns with the behavior of `@rollup/plugin-json`.
*/
defaultExport = uniqName("export_default");
code.append([
`\ndeclare const ${defaultExport}: {`,
[...exportedNames].map(name => ` ${name}: typeof ${name};`).join("\n"),
`};`,
`export default ${defaultExport};\n`
].join('\n')
);
}
for (const [fileId, importName] of inlineImports.entries()) {
code.prepend(`import * as ${importName} from "${fileId}";\n`);
}
Expand Down
1 change: 1 addition & 0 deletions tests/testcases/import-json/bar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["bar"]
8 changes: 8 additions & 0 deletions tests/testcases/import-json/expected.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare let name: string;
declare let age: number;
declare const export_default: {
name: typeof name;
age: typeof age;
};
declare const _exports: string[];
export { age, _exports as bar, export_default as foo, name };
4 changes: 4 additions & 0 deletions tests/testcases/import-json/foo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "foo",
"age": 42
}
6 changes: 6 additions & 0 deletions tests/testcases/import-json/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import foo from './foo.json'
import bar from './bar.json'

export { name, age } from './foo.json'

export { foo, bar }
18 changes: 18 additions & 0 deletions tests/testcases/import-json/meta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @ts-check
import url from "url";
import path from "path";

/** @type {import('../../testcases').Meta} */
export default {
/**
* TypeScript <5.1 will generate `declare const name: ...` for JSON modules,
* instead of current `decalre let name: ...`.
* But it doesn't matter, both can work.
* I'll just omit the test results for TypeScript <5.1.
*/
tsVersion: "5.1",
options: {
tsconfig: path.resolve(url.fileURLToPath(new URL(".", import.meta.url)), "tsconfig.json"),
},
rollupOptions: {},
};
21 changes: 21 additions & 0 deletions tests/testcases/import-json/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"resolveJsonModule": true,

"lib": ["es2018", "esnext", "dom"],
"skipDefaultLibCheck": true,

"strict": true,

"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"module": "esnext",
"stripInternal": true,

"newLine": "lf",

"noEmit": true,
"declaration": true,
"emitDeclarationOnly": true
}
}