Skip to content

Commit

Permalink
Import module specifier ending (#139)
Browse files Browse the repository at this point in the history
* Add config option to add extension to import paths

* Add importModuleSpecifierEnding to changelog

* importModuleSpecifierEnding for docs and playground
  • Loading branch information
captbaritone committed Apr 20, 2024
1 parent 0ec733b commit ce8c8a1
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 24 deletions.
3 changes: 2 additions & 1 deletion src/CHANGELOG.md
Expand Up @@ -6,8 +6,9 @@ Changes in this section are not yet released. If you need access to these change

- **Features**
- Code actions are now available to automatically fix some errors. These are available in the playground as well as in the experimental TypeScript plugin
- We now require that `__typename = "SomeType"` include `as const` to ensure no other typename can be assigned. A code fix is available.
- We now require that `__typename = "SomeType"` include `as const` to ensure no other typename can be assigned. A code fix is available
- Fields can now be defined using static methods, similar to how fields can be defined using functions
- Adds `importModuleSpecifierEnding` configuration option to enable users generating ES modules to add the `.js` file extension to import paths in the generated TypeScript schema file
- **Bug Fixes**
- Revert accidental breakage of the experimental TypeScript plugin
- Fix a bug where we generated incorrect import paths on Windows
Expand Down
27 changes: 16 additions & 11 deletions src/codegen.ts
Expand Up @@ -37,34 +37,38 @@ import {
createAssertNonNullHelper,
} from "./codegenHelpers";
import { extend, nullThrows } from "./utils/helpers";
import { ConfigOptions } from "./gratsConfig.js";

const RESOLVER_ARGS = ["source", "args", "context", "info"];

const F = ts.factory;

// Given a GraphQL SDL, returns the a string of TypeScript code that generates a
// GraphQLSchema implementing that schema.
export function codegen(schema: GraphQLSchema, destination: string): string {
const codegen = new Codegen(schema, destination);
export function codegen(
schema: GraphQLSchema,
config: ConfigOptions,
destination: string,
): string {
const codegen = new Codegen(schema, config, destination);

codegen.schemaDeclarationExport();

return codegen.print();
}

class Codegen {
_schema: GraphQLSchema;
_destination: string;
_imports: ts.Statement[] = [];
_helpers: Map<string, ts.Statement> = new Map();
_typeDefinitions: Set<string> = new Set();
_graphQLImports: Set<string> = new Set();
_statements: ts.Statement[] = [];

constructor(schema: GraphQLSchema, destination: string) {
this._schema = schema;
this._destination = destination;
}
constructor(
public _schema: GraphQLSchema,
public _config: ConfigOptions,
public _destination: string,
) {}

createBlockWithScope(closure: () => void): ts.Block {
const initialStatements = this._statements;
Expand Down Expand Up @@ -225,8 +229,9 @@ class Codegen {
const argCount = nullThrows(metadata.argCount);

const abs = resolveRelativePath(module);
const relative = stripExt(
const relative = replaceExt(
path.relative(path.dirname(this._destination), abs),
this._config.importModuleSpecifierEnding,
);

// Note: This name is guaranteed to be unique, but for static methods, it
Expand Down Expand Up @@ -878,9 +883,9 @@ function fieldDirective(
return field.astNode?.directives?.find((d) => d.name.value === name) ?? null;
}

function stripExt(filePath: string): string {
function replaceExt(filePath: string, newSuffix: string): string {
const ext = path.extname(filePath);
return filePath.slice(0, -ext.length);
return filePath.slice(0, -ext.length) + newSuffix;
}

// Predicate function for filtering out null values
Expand Down
16 changes: 16 additions & 0 deletions src/gratsConfig.ts
Expand Up @@ -45,6 +45,13 @@ export type ConfigOptions = {
// headers or other information to the generated file. Set to `null` to omit
// the default header.
tsSchemaHeader: string | null; // Defaults to info about Grats

// This option allows you configure an extension that will be appended
// to the end of all import paths in the generated TypeScript schema file.
// When building a package that uses ES modules, import paths must not omit the
// file extension. In TypeScript code this generally means import paths must end
// with `.js`. If set to null, no ending will be appended.
importModuleSpecifierEnding: string; // Defaults to no ending, or ""
};

export type ParsedCommandLineGrats = Omit<ts.ParsedCommandLine, "raw"> & {
Expand All @@ -69,6 +76,7 @@ const VALID_CONFIG_KEYS = new Set([
"reportTypeScriptTypeErrors",
"schemaHeader",
"tsSchemaHeader",
"importModuleSpecifierEnding",
]);

// TODO: Make this return diagnostics
Expand Down Expand Up @@ -175,6 +183,14 @@ export function validateGratsOptions(
);
}

if (gratsOptions.importModuleSpecifierEnding === undefined) {
gratsOptions.importModuleSpecifierEnding = "";
} else if (typeof gratsOptions.importModuleSpecifierEnding !== "string") {
throw new Error(
"Grats: The Grats config option `importModuleSpecifierEnding` must be a string if provided.",
);
}

return {
...options,
raw: { ...options.raw, grats: gratsOptions },
Expand Down
4 changes: 3 additions & 1 deletion src/lib.ts
Expand Up @@ -17,7 +17,7 @@ import * as ts from "typescript";
import { ExtractionSnapshot } from "./Extractor";
import { TypeContext } from "./TypeContext";
import { validateSDL } from "graphql/validation/validate";
import { ParsedCommandLineGrats } from "./gratsConfig";
import { ConfigOptions, ParsedCommandLineGrats } from "./gratsConfig";
import { validateTypenames } from "./validations/validateTypenames";
import { extractSnapshotsFromProgram } from "./transforms/snapshotsFromProgram";
import { validateMergedInterfaces } from "./validations/validateMergedInterfaces";
Expand All @@ -36,6 +36,8 @@ import { resolveTypes } from "./transforms/resolveTypes";
// grats-ts-plugin
export { initTsPlugin } from "./tsPlugin/initTsPlugin";

export type GratsConfig = ConfigOptions;

export type SchemaAndDoc = {
schema: GraphQLSchema;
doc: DocumentNode;
Expand Down
2 changes: 1 addition & 1 deletion src/printSchema.ts
Expand Up @@ -18,7 +18,7 @@ export function printExecutableSchema(
config: ConfigOptions,
destination: string,
): string {
const code = codegen(schema, destination);
const code = codegen(schema, config, destination);
return applyTypeScriptHeader(config, code);
}

Expand Down
12 changes: 12 additions & 0 deletions src/tests/fixtures/configOptions/importModuleSpecifierEnding.ts
@@ -0,0 +1,12 @@
// {"importModuleSpecifierEnding": ".js"}

/** @gqlType */
export default class SomeType {
/** @gqlField */
hello: string;
}

/** @gqlField */
export function greeting(t: SomeType): string {
return t.hello + " world!";
}
@@ -0,0 +1,50 @@
-----------------
INPUT
-----------------
// {"importModuleSpecifierEnding": ".js"}

/** @gqlType */
export default class SomeType {
/** @gqlField */
hello: string;
}

/** @gqlField */
export function greeting(t: SomeType): string {
return t.hello + " world!";
}

-----------------
OUTPUT
-----------------
-- SDL --
type SomeType {
greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/configOptions/importModuleSpecifierEnding.ts")
hello: String @metadata
}
-- TypeScript --
import { greeting as someTypeGreetingResolver } from "./importModuleSpecifierEnding.js";
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";
export function getSchema(): GraphQLSchema {
const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({
name: "SomeType",
fields() {
return {
greeting: {
name: "greeting",
type: GraphQLString,
resolve(source) {
return someTypeGreetingResolver(source);
}
},
hello: {
name: "hello",
type: GraphQLString
}
};
}
});
return new GraphQLSchema({
types: [SomeTypeType]
});
}
4 changes: 2 additions & 2 deletions src/tests/test.ts
Expand Up @@ -139,7 +139,7 @@ const testDirs = [
// We run codegen here just ensure that it doesn't throw.
const executableSchema = applyTypeScriptHeader(
parsedOptions.raw.grats,
codegen(schema, `${fixturesDir}/${fileName}`),
codegen(schema, parsedOptions.raw.grats, `${fixturesDir}/${fileName}`),
);

const LOCATION_REGEX = /^\/\/ Locate: (.*)/;
Expand Down Expand Up @@ -218,7 +218,7 @@ const testDirs = [

const { schema, doc } = schemaResult.value;

const tsSchema = codegen(schema, schemaPath);
const tsSchema = codegen(schema, parsedOptions.raw.grats, schemaPath);

writeFileSync(schemaPath, tsSchema);

Expand Down
9 changes: 8 additions & 1 deletion website/docs/01-getting-started/03-configuration.md
Expand Up @@ -52,7 +52,14 @@ Grats has a few configuration options. They can be set under the `grats` key in
// of strings. These strings will be joined together.
//
// Set to `null` to omit the default header.
"tsSchemaHeader": "/** Copyright SomeCorp, 1998..." // Defaults to info about Grats
"tsSchemaHeader": "/** Copyright SomeCorp, 1998...", // Defaults to info about Grats

// This option allows you configure an extension that will be appended
// to the end of all import paths in the generated TypeScript schema file.
// When building a package that uses ES modules, import paths must not omit the
// file extension. In TypeScript code this generally means import paths must end
// with `.js`. If set to null, no ending will be appended.
"importModuleSpecifierEnding": ".js" // Defaults to no ending, or ""
},
"compilerOptions": {
// ... TypeScript config...
Expand Down
3 changes: 2 additions & 1 deletion website/scripts/gratsCode.js
Expand Up @@ -20,6 +20,7 @@ function processFile(file) {
let options = {
nullableByDefault: true,
reportTypeScriptTypeErrors: true,
importModuleSpecifierEnding: "",
};
const parsedOptions = {
options: {},
Expand All @@ -37,7 +38,7 @@ function processFile(file) {
}

const { doc, schema } = schemaAndDocResult.value;
const typeScript = codegen(schema, file, options);
const typeScript = codegen(schema, options, file);
const graphql = printSDLWithoutMetadata(doc);

const fileContent = fs.readFileSync(file, "utf8");
Expand Down
15 changes: 9 additions & 6 deletions website/src/components/PlaygroundFeatures/linter.ts
@@ -1,6 +1,6 @@
import { createSystem, createVirtualCompilerHost } from "@typescript/vfs";
import * as ts from "typescript";
import { buildSchemaAndDocResultWithHost } from "grats/src/lib";
import { buildSchemaAndDocResultWithHost, GratsConfig } from "grats/src/lib";
import { codegen } from "grats/src/codegen";
import { ReportableDiagnostics } from "grats/src/utils/DiagnosticError";
import { printSDLWithoutMetadata } from "grats/src/printSchema";
Expand All @@ -26,7 +26,7 @@ if (ExecutionEnvironment.canUseDOM) {
};
}

function buildSchemaResultWithFsMap(fsMap, text: string, config) {
function buildSchemaResultWithFsMap(fsMap, text: string, config: GratsConfig) {
fsMap.set("index.ts", text);
fsMap.set(GRATS_PATH, GRATS_TYPE_DECLARATIONS);
// TODO: Don't recreate the system each time!
Expand Down Expand Up @@ -65,7 +65,7 @@ function buildSchemaResultWithFsMap(fsMap, text: string, config) {
}
}

export function createLinter(fsMap, view, config) {
export function createLinter(fsMap, view, config: GratsConfig) {
return linter((codeMirrorView) => {
const text = codeMirrorView.viewState.state.doc.toString();

Expand Down Expand Up @@ -100,7 +100,7 @@ export function createLinter(fsMap, view, config) {
});
}

const codegenOutput = computeCodegenOutput(result.value.schema);
const codegenOutput = computeCodegenOutput(result.value.schema, config);
const output = computeOutput(result.value.doc, view);

store.dispatch({
Expand All @@ -123,8 +123,11 @@ function computeOutput(
return print(doc);
}

function computeCodegenOutput(schema: GraphQLSchema): string {
return codegen(schema, "./schema.ts");
function computeCodegenOutput(
schema: GraphQLSchema,
config: GratsConfig,
): string {
return codegen(schema, config, "./schema.ts");
}

function commentLines(text: string): string {
Expand Down

0 comments on commit ce8c8a1

Please sign in to comment.