graphql-mini-transforms
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -8,2 +8,11 @@ # Changelog | ||
## [1.2.0] - 2020-04-27 | ||
### Added | ||
- Added a new `{simple: true}` option to the `graphql-mini-transforms/webpack` loader to produce GraphQL exports without any AST [[#114](https://github.com/Shopify/graphql-tools-web/pull/114)] | ||
- Added a new `graphql-mini-transforms/jest-simple` transformer that produces the same shape as the webpack loader’s `simple` option [[#114](https://github.com/Shopify/graphql-tools-web/pull/114)] | ||
## [1.1.0] - 2020-04-14 | ||
### Changed | ||
@@ -10,0 +19,0 @@ |
@@ -1,5 +0,6 @@ | ||
import { DocumentNode } from 'graphql'; | ||
export declare function cleanDocument(document: DocumentNode, { removeUnused }?: { | ||
import { DocumentNode as UntypedDocumentNode } from 'graphql'; | ||
import { DocumentNode, SimpleDocument } from 'graphql-typed'; | ||
export declare function cleanDocument(document: UntypedDocumentNode, { removeUnused }?: { | ||
removeUnused?: boolean | undefined; | ||
}): DocumentNode; | ||
}): DocumentNode<any, any, any>; | ||
export declare function extractImports(rawSource: string): { | ||
@@ -9,1 +10,2 @@ imports: string[]; | ||
}; | ||
export declare function toSimpleDocument<Data, Variables, DeepPartial>(document: DocumentNode<Data, Variables, DeepPartial>): SimpleDocument<Data, Variables, DeepPartial>; |
@@ -47,2 +47,15 @@ "use strict"; | ||
exports.extractImports = extractImports; | ||
function toSimpleDocument(document) { | ||
var _a, _b; | ||
return { | ||
id: document.id, | ||
name: operationNameForDocument(document), | ||
source: (_b = (_a = document.loc) === null || _a === void 0 ? void 0 : _a.source) === null || _b === void 0 ? void 0 : _b.body, | ||
}; | ||
} | ||
exports.toSimpleDocument = toSimpleDocument; | ||
function operationNameForDocument(document) { | ||
var _a, _b; | ||
return (_b = (_a = document.definitions.find((definition) => definition.kind === 'OperationDefinition')) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.value; | ||
} | ||
function removeUnusedDefinitions(document) { | ||
@@ -49,0 +62,0 @@ const usedDefinitions = new Set(); |
@@ -14,2 +14,3 @@ "use strict"; | ||
const graphql_1 = require("graphql"); | ||
const loader_utils_1 = require("loader-utils"); | ||
const document_1 = require("./document"); | ||
@@ -20,2 +21,3 @@ function graphQLLoader(source) { | ||
const done = this.async(); | ||
const { simple = false } = loader_utils_1.getOptions(this); | ||
if (done == null) { | ||
@@ -25,4 +27,5 @@ throw new Error('@shopify/graphql-loader does not support synchronous processing'); | ||
try { | ||
const document = yield loadDocument(source, this.context, this); | ||
done(null, `export default ${JSON.stringify(document_1.cleanDocument(document))};`); | ||
const document = document_1.cleanDocument(yield loadDocument(source, this.context, this)); | ||
const exported = simple ? document_1.toSimpleDocument(document) : document; | ||
done(null, `export default JSON.parse(${JSON.stringify(JSON.stringify(exported))});`); | ||
} | ||
@@ -29,0 +32,0 @@ catch (error) { |
{ | ||
"name": "graphql-mini-transforms", | ||
"description": "Transformers for importing .graphql files in various build tools.", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"types": "lib", | ||
@@ -34,4 +34,5 @@ "license": "MIT", | ||
"devDependencies": { | ||
"@types/common-tags": "1.8.0", | ||
"common-tags": "1.8.0" | ||
"@types/common-tags": "^1.8.0", | ||
"@types/loader-utils": "^1.1.3", | ||
"common-tags": "^1.8.0" | ||
}, | ||
@@ -43,4 +44,6 @@ "dependencies": { | ||
"fs-extra": "^9.0.0", | ||
"graphql": ">=14.5.0 <15.0.0" | ||
"graphql": ">=14.5.0 <15.0.0", | ||
"graphql-typed": "^0.6.0", | ||
"loader-utils": "^2.0.0" | ||
} | ||
} |
@@ -62,2 +62,23 @@ # `graphql-mini-transforms` | ||
#### Options | ||
This loader accepts a single option, `simple`. This option changes the shape of the value exported from `.graphql` files. By default, a `graphql-typed` `DocumentNode` is exported, but when `simple` is set to `true`, a `SimpleDocument` is exported instead. This representation of GraphQL documents is smaller than a full `DocumentNode`, but generally won’t work with normalized GraphQL caches. | ||
```js | ||
module.exports = { | ||
module: { | ||
rules: [ | ||
{ | ||
test: /\.(graphql|gql)$/, | ||
use: 'graphql-mini-transforms/webpack', | ||
exclude: /node_modules/, | ||
options: {simple: true}, | ||
}, | ||
], | ||
}, | ||
}; | ||
``` | ||
If this option is set to `true`, you should also use the `jest-simple` transformer for Jest, and the `--export-format simple` flag for `graphql-typescript-definitions`. | ||
### Jest | ||
@@ -75,2 +96,12 @@ | ||
If you want to get the same output as the `simple` option of the webpack loader, you can instead use the `jest-simple` loader transformer: | ||
```js | ||
module.exports = { | ||
transform: { | ||
'\\.(gql|graphql)$': 'graphql-mini-transforms/jest-simple', | ||
}, | ||
}; | ||
``` | ||
## Prior art | ||
@@ -91,3 +122,1 @@ | ||
- [next-plugin-mini-graphql](https://www.npmjs.com/package/next-plugin-mini-graphql) - Provides [Next.js](https://nextjs.org/) support for `.graphql` files using `graphql-mini-transforms` | ||
@@ -1,5 +0,6 @@ | ||
import { DocumentNode } from 'graphql'; | ||
export declare function cleanDocument(document: DocumentNode, { removeUnused }?: { | ||
import { DocumentNode as UntypedDocumentNode } from 'graphql'; | ||
import { DocumentNode, SimpleDocument } from 'graphql-typed'; | ||
export declare function cleanDocument(document: UntypedDocumentNode, { removeUnused }?: { | ||
removeUnused?: boolean | undefined; | ||
}): DocumentNode; | ||
}): DocumentNode<any, any, any>; | ||
export declare function extractImports(rawSource: string): { | ||
@@ -9,1 +10,2 @@ imports: string[]; | ||
}; | ||
export declare function toSimpleDocument<Data, Variables, DeepPartial>(document: DocumentNode<Data, Variables, DeepPartial>): SimpleDocument<Data, Variables, DeepPartial>; |
@@ -47,2 +47,15 @@ "use strict"; | ||
exports.extractImports = extractImports; | ||
function toSimpleDocument(document) { | ||
var _a, _b; | ||
return { | ||
id: document.id, | ||
name: operationNameForDocument(document), | ||
source: (_b = (_a = document.loc) === null || _a === void 0 ? void 0 : _a.source) === null || _b === void 0 ? void 0 : _b.body, | ||
}; | ||
} | ||
exports.toSimpleDocument = toSimpleDocument; | ||
function operationNameForDocument(document) { | ||
var _a, _b; | ||
return (_b = (_a = document.definitions.find((definition) => definition.kind === 'OperationDefinition')) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.value; | ||
} | ||
function removeUnusedDefinitions(document) { | ||
@@ -49,0 +62,0 @@ const usedDefinitions = new Set(); |
@@ -6,9 +6,11 @@ import {createHash} from 'crypto'; | ||
parse, | ||
DocumentNode, | ||
DocumentNode as UntypedDocumentNode, | ||
DefinitionNode, | ||
SelectionSetNode, | ||
ExecutableDefinitionNode, | ||
OperationDefinitionNode, | ||
SelectionNode, | ||
Location, | ||
} from 'graphql'; | ||
import {DocumentNode, SimpleDocument} from 'graphql-typed'; | ||
@@ -19,5 +21,5 @@ const IMPORT_REGEX = /^#import\s+['"]([^'"]*)['"];?[\s\n]*/gm; | ||
export function cleanDocument( | ||
document: DocumentNode, | ||
document: UntypedDocumentNode, | ||
{removeUnused = true} = {}, | ||
) { | ||
): DocumentNode<any, any, any> { | ||
if (removeUnused) { | ||
@@ -57,3 +59,3 @@ removeUnusedDefinitions(document); | ||
return normalizedDocument; | ||
return normalizedDocument as any; | ||
} | ||
@@ -72,3 +74,20 @@ | ||
function removeUnusedDefinitions(document: DocumentNode) { | ||
export function toSimpleDocument<Data, Variables, DeepPartial>( | ||
document: DocumentNode<Data, Variables, DeepPartial>, | ||
): SimpleDocument<Data, Variables, DeepPartial> { | ||
return { | ||
id: document.id, | ||
name: operationNameForDocument(document), | ||
source: document.loc?.source?.body!, | ||
}; | ||
} | ||
function operationNameForDocument(document: DocumentNode) { | ||
return document.definitions.find( | ||
(definition): definition is OperationDefinitionNode => | ||
definition.kind === 'OperationDefinition', | ||
)?.name?.value; | ||
} | ||
function removeUnusedDefinitions(document: UntypedDocumentNode) { | ||
const usedDefinitions = new Set<DefinitionNode>(); | ||
@@ -75,0 +94,0 @@ const dependencies = definitionDependencies(document.definitions); |
@@ -14,2 +14,3 @@ "use strict"; | ||
const graphql_1 = require("graphql"); | ||
const loader_utils_1 = require("loader-utils"); | ||
const document_1 = require("./document"); | ||
@@ -20,2 +21,3 @@ function graphQLLoader(source) { | ||
const done = this.async(); | ||
const { simple = false } = loader_utils_1.getOptions(this); | ||
if (done == null) { | ||
@@ -25,4 +27,5 @@ throw new Error('@shopify/graphql-loader does not support synchronous processing'); | ||
try { | ||
const document = yield loadDocument(source, this.context, this); | ||
done(null, `export default ${JSON.stringify(document_1.cleanDocument(document))};`); | ||
const document = document_1.cleanDocument(yield loadDocument(source, this.context, this)); | ||
const exported = simple ? document_1.toSimpleDocument(document) : document; | ||
done(null, `export default JSON.parse(${JSON.stringify(JSON.stringify(exported))});`); | ||
} | ||
@@ -29,0 +32,0 @@ catch (error) { |
@@ -5,5 +5,10 @@ import {dirname} from 'path'; | ||
import {parse, DocumentNode} from 'graphql'; | ||
import {getOptions} from 'loader-utils'; | ||
import {cleanDocument, extractImports} from './document'; | ||
import {cleanDocument, extractImports, toSimpleDocument} from './document'; | ||
interface Options { | ||
simple?: boolean; | ||
} | ||
export default async function graphQLLoader( | ||
@@ -16,2 +21,3 @@ this: loader.LoaderContext, | ||
const done = this.async(); | ||
const {simple = false} = getOptions(this) as Options; | ||
@@ -25,4 +31,11 @@ if (done == null) { | ||
try { | ||
const document = await loadDocument(source, this.context, this); | ||
done(null, `export default ${JSON.stringify(cleanDocument(document))};`); | ||
const document = cleanDocument( | ||
await loadDocument(source, this.context, this), | ||
); | ||
const exported = simple ? toSimpleDocument(document) : document; | ||
done( | ||
null, | ||
`export default JSON.parse(${JSON.stringify(JSON.stringify(exported))});`, | ||
); | ||
} catch (error) { | ||
@@ -29,0 +42,0 @@ done(error); |
@@ -151,8 +151,39 @@ "use strict"; | ||
}); | ||
describe('simple', () => { | ||
it('has a source property that is the minified source', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const originalSource = common_tags_1.stripIndent ` | ||
# Comments should go away | ||
# As should extra space | ||
query Shop ( $id : ID! , $first: Number! ) { | ||
# Most whitespace should go too | ||
shop ( id: $id, first: $first ) { | ||
# Should also minify selection sets | ||
id, | ||
name, | ||
} | ||
} | ||
`; | ||
const expectedSource = `query Shop($id:ID!,$first:Number!){shop(id:$id,first:$first){id name __typename}}`; | ||
expect(yield extractDocumentExport(originalSource, createLoaderContext({ query: { simple: true } }))).toHaveProperty('source', expectedSource); | ||
})); | ||
it('has an id property that is a sha256 hash of the query document', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const result = yield extractDocumentExport(`query Shop { shop { id } }`, createLoaderContext({ query: { simple: true } })); | ||
expect(result).toHaveProperty('id', crypto_1.createHash('sha256').update(result.source).digest('hex')); | ||
})); | ||
it('has a name property that is the name of the first operation', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const result = yield extractDocumentExport(`query Shop { shop { id } }`, createLoaderContext({ query: { simple: true } })); | ||
expect(result).toHaveProperty('name', 'Shop'); | ||
})); | ||
it('has an undefined name when there are no named operations', () => __awaiter(void 0, void 0, void 0, function* () { | ||
const result = yield extractDocumentExport(`query { shop { id } }`, createLoaderContext({ query: { simple: true } })); | ||
expect(result).toHaveProperty('name', undefined); | ||
})); | ||
}); | ||
}); | ||
// This is a limited subset of the loader API that we actually use in our | ||
// loader. | ||
function createLoaderContext({ context = __dirname, readFile = () => '', resolve = (context, imported) => path.resolve(context, imported), } = {}) { | ||
function createLoaderContext({ query = {}, context = __dirname, readFile = () => '', resolve = (context, imported) => path.resolve(context, imported), } = {}) { | ||
return { | ||
context, | ||
query, | ||
fs: { | ||
@@ -188,3 +219,4 @@ readFile(file, withFile) { | ||
const result = yield simulateRun(source, loader); | ||
return JSON.parse(result.replace(/^export default /, '').replace(/;$/, '')); | ||
// eslint-disable-next-line no-eval | ||
return eval(result.replace(/^export default /, '').replace(/;$/, '')); | ||
}); | ||
@@ -191,0 +223,0 @@ } |
@@ -203,5 +203,62 @@ import * as path from 'path'; | ||
}); | ||
describe('simple', () => { | ||
it('has a source property that is the minified source', async () => { | ||
const originalSource = stripIndent` | ||
# Comments should go away | ||
# As should extra space | ||
query Shop ( $id : ID! , $first: Number! ) { | ||
# Most whitespace should go too | ||
shop ( id: $id, first: $first ) { | ||
# Should also minify selection sets | ||
id, | ||
name, | ||
} | ||
} | ||
`; | ||
const expectedSource = `query Shop($id:ID!,$first:Number!){shop(id:$id,first:$first){id name __typename}}`; | ||
expect( | ||
await extractDocumentExport( | ||
originalSource, | ||
createLoaderContext({query: {simple: true}}), | ||
), | ||
).toHaveProperty('source', expectedSource); | ||
}); | ||
it('has an id property that is a sha256 hash of the query document', async () => { | ||
const result = await extractDocumentExport( | ||
`query Shop { shop { id } }`, | ||
createLoaderContext({query: {simple: true}}), | ||
); | ||
expect(result).toHaveProperty( | ||
'id', | ||
createHash('sha256').update(result.source).digest('hex'), | ||
); | ||
}); | ||
it('has a name property that is the name of the first operation', async () => { | ||
const result = await extractDocumentExport( | ||
`query Shop { shop { id } }`, | ||
createLoaderContext({query: {simple: true}}), | ||
); | ||
expect(result).toHaveProperty('name', 'Shop'); | ||
}); | ||
it('has an undefined name when there are no named operations', async () => { | ||
const result = await extractDocumentExport( | ||
`query { shop { id } }`, | ||
createLoaderContext({query: {simple: true}}), | ||
); | ||
expect(result).toHaveProperty('name', undefined); | ||
}); | ||
}); | ||
}); | ||
interface Options { | ||
query?: any; | ||
context?: string; | ||
@@ -215,2 +272,3 @@ resolve?(context: string, imported: string): string | Error; | ||
function createLoaderContext({ | ||
query = {}, | ||
context = __dirname, | ||
@@ -222,2 +280,3 @@ readFile = () => '', | ||
context, | ||
query, | ||
fs: { | ||
@@ -263,3 +322,5 @@ readFile( | ||
const result = await simulateRun(source, loader); | ||
return JSON.parse(result.replace(/^export default /, '').replace(/;$/, '')); | ||
// eslint-disable-next-line no-eval | ||
return eval(result.replace(/^export default /, '').replace(/;$/, '')); | ||
} | ||
@@ -266,0 +327,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
65825
30
1592
120
7
3
4
1
+ Addedgraphql-typed@^0.6.0
+ Addedloader-utils@^2.0.0
+ Addedbig.js@5.2.2(transitive)
+ Addedemojis-list@3.0.0(transitive)
+ Addedgraphql-typed@0.6.1(transitive)
+ Addedloader-utils@2.0.4(transitive)