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

Richer import syntax #9

Merged
merged 3 commits into from
Jul 26, 2019
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Given the following .graphql files:
#### my-query.graphql

```graphql
#import "./my-fragment.gql"
# import * from "./my-fragment.gql"

query myQuery {
foo {
Expand Down Expand Up @@ -140,6 +140,30 @@ const doc = {
export default doc;
```

### Import Syntax

Imports of fragments from other locations are specified using comments in a format compatible with a subset of [`graphql-import`](https://oss.prisma.io/content/graphql-import/overview).

To bring all identifiers in a specific module into scope, you can use `*`:

```graphql
# import * from 'path/to/module'
```

To only import specific identifiers, you can write them out separated by commas:

```graphql
# import Foo from 'path/to/foo'
# import Bar, Baz from 'path/to/bar-baz'
```

#### Migrating Import Syntax

Up to `v0.3.2`, `broccoli-graphql-filter` used a coarser-grained import syntax.
In order to align with the broader ecosystem and enable better static analysis opportunities, we've moved to a subset of [`graphql-import`](https://oss.prisma.io/content/graphql-import/overview)'s syntax.

To keep the semantics of your existing imports while migrating to the new syntax, you can perform project-wide find/replace, replacing all instances of `#import` in your project's GraphQL documents with `# import * from`.

## Acknowledgements

The filter code was extracted from https://github.com/bgentry/ember-apollo-client and was originally contributed by https://github.com/dfreeman.
18 changes: 10 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const Filter = require("broccoli-persistent-filter");
const gql = require("graphql-tag");
const extractImports = require("./lib/extract-imports");

gql.disableFragmentWarnings();

Expand All @@ -21,20 +22,21 @@ module.exports = class GraphQLFilter extends Filter {
return `${newPath}.js`;
}

processString(source) {
processString(source, relativePath) {
let output = [
`const doc = ${JSON.stringify(gql([source]), null, 2)};`,
`export default doc;`
];

source.split("\n").forEach((line, i) => {
let match = /^#import\s+(.*)/.exec(line);
if (match && match[1]) {
output.push(`import dep${i} from ${match[1]};`);
output.push(
`doc.definitions = doc.definitions.concat(dep${i}.definitions);`
);
extractImports(source, relativePath).forEach((directive, i) => {
let definitions = `dep${i}.definitions`;
if (directive.bindings) {
let matcher = `/^(${directive.bindings.join("|")})$/`;
definitions = `${definitions}.filter(def => ${matcher}.test(def.name.value))`;
}

output.push(`import dep${i} from ${directive.source};`);
output.push(`doc.definitions = doc.definitions.concat(${definitions});`);
});

return output.join("\n") + "\n";
Expand Down
45 changes: 45 additions & 0 deletions lib/extract-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
let importRegex = /^#\s*import\s+(.*?)\s+from\s+(.*)/;
let legacyImportRegex = /^#import\s+(.*)/;

module.exports = function extractImports(source, filePath) {
let lines = source.split("\n");
let imports = [];
let warned = false;

for (let line of lines) {
let match;
if (match = importRegex.exec(line)) {
let source = match[2];
let bindings = match[1].split(/\s*,\s*/);
if (bindings.length === 1 && bindings[0] === "*") {
imports.push({ source });
} else {
validateBindings(bindings);
imports.push({ source, bindings });
}
} else if (match = legacyImportRegex.exec(line)) {
imports.push({ source: match[1] });
if (!warned) {
warned = true;
console.warn(
`[DEPRECATION] Legacy import syntax found in ${filePath}. ` +
'See https://git.io/fjym5 for migration details.'
);
}
}
}

return imports;
}

function validateBindings(bindings) {
for (let binding of bindings) {
if (binding === '*') {
throw new Error("A '*' import must be the only binding");
}

if (!/^[a-z_][a-z0-9_]*$/i.test(binding)) {
throw new Error(`Invalid import identifier: ${binding}`);
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"devDependencies": {
"broccoli": "^2.0.1",
"common-tags": "^1.8.0",
"mocha": "^5.2.0"
},
"scripts": {
Expand Down
71 changes: 70 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

const assert = require("assert");
const fs = require("fs");
const { stripIndent } = require("common-tags");
const { Builder } = require("broccoli");

const GraphQLFilter = require(".");
const extractImports = require("./lib/extract-imports");

const FIXTURES_DIR = `${__dirname}/test/fixtures`;
const FIXTURES = fs.readdirSync(FIXTURES_DIR);

describe("File creation", function() {
describe("Acceptance | File creation", function() {
for (let fixture of FIXTURES) {
describe(fixture, function() {
const cwd = `${FIXTURES_DIR}/${fixture}`;
Expand Down Expand Up @@ -43,3 +46,69 @@ describe("File creation", function() {
});
}
});

describe("Unit | extractImports", function() {
it("extracts * imports", function() {
let source = stripIndent`
# import * from "./foo"
`;

assert.deepEqual(extractImports(source, 'test.gql'), [
{ source: `"./foo"` }
]);
});

it("extracts named imports", function() {
let source = stripIndent`
# import Foo, Bar from "./baz"
`;

assert.deepEqual(extractImports(source, 'test.gql'), [
{ source: `"./baz"`, bindings: ["Foo", "Bar"] }
]);
});

it("extracts legacy-format imports", function() {
let source = stripIndent`
#import "./foo"
`;

assert.deepEqual(extractImports(source, 'test.gql'), [
{ source: `"./foo"` }
]);
});

it("extracts all imports", function() {
let source = stripIndent`
# import * from "./foo"
# import Bar from "baz"
`;

assert.deepEqual(extractImports(source, 'test.gql'), [
{ source: `"./foo"` },
{ source: `"baz"`, bindings: ["Bar"] }
]);
});

it("rejects invalid import specifiers", function() {
let source = stripIndent`
# import Foo, * from "./foo"
`;

assert.throws(
() => extractImports(source, 'test.gql'),
new Error("A '*' import must be the only binding")
);
});

it("rejects invalid identifiers", function() {
let source = stripIndent`
# import foo-bar from "./foo"
`;

assert.throws(
() => extractImports(source, 'test.gql'),
new Error("Invalid import identifier: foo-bar")
);
});
});
2 changes: 1 addition & 1 deletion test/fixtures/extension-gql/expected/my-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const doc = {
],
"loc": {
"start": 0,
"end": 73
"end": 81
}
};
export default doc;
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/extension-gql/input/my-query.gql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#import "./my-fragment"
# import * from "./my-fragment"

query MyQuery {
foo {
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/extension-graphql/expected/my-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const doc = {
],
"loc": {
"start": 0,
"end": 73
"end": 81
}
};
export default doc;
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/extension-graphql/input/my-query.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#import "./my-fragment"
# import * from "./my-fragment"

query MyQuery {
foo {
Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/import-legacy/expected/my-fragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const doc = {
"kind": "Document",
"definitions": [
{
"kind": "FragmentDefinition",
"name": {
"kind": "Name",
"value": "MyFragment"
},
"typeCondition": {
"kind": "NamedType",
"name": {
"kind": "Name",
"value": "Foo"
}
},
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hello"
},
"arguments": [],
"directives": []
}
]
}
}
],
"loc": {
"start": 0,
"end": 39
}
};
export default doc;
49 changes: 49 additions & 0 deletions test/fixtures/import-legacy/expected/my-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const doc = {
"kind": "Document",
"definitions": [
{
"kind": "OperationDefinition",
"operation": "query",
"name": {
"kind": "Name",
"value": "MyQuery"
},
"variableDefinitions": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "foo"
},
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "FragmentSpread",
"name": {
"kind": "Name",
"value": "MyFragment"
},
"directives": []
}
]
}
}
]
}
}
],
"loc": {
"start": 0,
"end": 73
}
};
export default doc;
import dep0 from "./my-fragment";
doc.definitions = doc.definitions.concat(dep0.definitions);
3 changes: 3 additions & 0 deletions test/fixtures/import-legacy/input/my-fragment.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fragment MyFragment on Foo {
hello
}
7 changes: 7 additions & 0 deletions test/fixtures/import-legacy/input/my-query.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#import "./my-fragment"

query MyQuery {
foo {
...MyFragment
}
}
Loading