@quilted/rollup 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,18 +4,31 @@ import { multiline } from './shared/strings.esnext';
4
4
  import { rollupPluginsToArray, getNodePlugins } from './shared/rollup.esnext';
5
5
  import { createMagicModulePlugin } from './shared/magic-module.esnext';
6
6
 
7
- function quiltApp({
7
+ function quiltAppBrowser({
8
+ app,
8
9
  env,
9
- entry
10
+ assets,
11
+ module,
12
+ graphql = true
10
13
  } = {}) {
14
+ const mode = (typeof env === 'object' ? env?.mode : undefined) ?? 'production';
11
15
  return {
12
- name: '@quilted/app',
16
+ name: '@quilted/app/browser',
13
17
  async options(originalOptions) {
14
18
  const newPlugins = rollupPluginsToArray(originalOptions.plugins);
15
19
  const newOptions = {
16
20
  ...originalOptions,
17
21
  plugins: newPlugins
18
22
  };
23
+ const [{
24
+ visualizer
25
+ }, {
26
+ sourceCode
27
+ }, nodePlugins] = await Promise.all([import('rollup-plugin-visualizer'), import('./shared/source-code.esnext'), getNodePlugins()]);
28
+ newPlugins.push(...nodePlugins);
29
+ newPlugins.push(sourceCode({
30
+ mode
31
+ }));
19
32
  if (env) {
20
33
  const {
21
34
  magicModuleEnv,
@@ -38,40 +51,20 @@ function quiltApp({
38
51
  }));
39
52
  }
40
53
  }
41
- if (entry) {
54
+ if (app) {
42
55
  newPlugins.push(magicModuleAppComponent({
43
- entry
56
+ entry: app
44
57
  }));
45
58
  }
46
- return newOptions;
47
- }
48
- };
49
- }
50
- function quiltAppBrowser({
51
- entry,
52
- env,
53
- graphql,
54
- assets,
55
- module
56
- } = {}) {
57
- return {
58
- name: '@quilted/app/browser',
59
- async options(originalOptions) {
60
- const newPlugins = rollupPluginsToArray(originalOptions.plugins);
61
- const newOptions = {
62
- ...originalOptions,
63
- plugins: newPlugins
64
- };
65
- const [{
66
- visualizer
67
- }, nodePlugins] = await Promise.all([import('rollup-plugin-visualizer'), getNodePlugins()]);
68
- newPlugins.push(quiltApp({
69
- env,
70
- entry,
71
- graphql
72
- }));
73
- newPlugins.push(...nodePlugins);
74
59
  newPlugins.push(magicModuleAppBrowserEntry(module));
60
+ if (graphql) {
61
+ const {
62
+ graphql
63
+ } = await import('./graphql.esnext');
64
+ newPlugins.push(graphql({
65
+ manifest: path.resolve(`manifests/graphql.json`)
66
+ }));
67
+ }
75
68
  const minify = assets?.minify ?? true;
76
69
  if (minify) {
77
70
  const {
@@ -83,13 +76,31 @@ function quiltAppBrowser({
83
76
  template: 'treemap',
84
77
  open: false,
85
78
  brotliSize: true,
86
- filename: path.resolve('reports', `bundle-visualizer.html`)
79
+ filename: path.resolve(`reports/bundle-visualizer.html`)
87
80
  }));
88
81
  return newOptions;
82
+ },
83
+ outputOptions(originalOptions) {
84
+ return {
85
+ ...originalOptions,
86
+ // format: isESM ? 'esm' : 'systemjs',
87
+ format: 'esm',
88
+ dir: path.resolve(`build/assets`),
89
+ entryFileNames: `app.[hash].js`,
90
+ assetFileNames: `[name].[hash].[ext]`,
91
+ chunkFileNames: `[name].[hash].js`,
92
+ manualChunks: createManualChunksSorter()
93
+ };
89
94
  }
90
95
  };
91
96
  }
92
- function quiltAppServer(options = {}) {
97
+ function quiltAppServer({
98
+ app,
99
+ env,
100
+ graphql,
101
+ entry
102
+ } = {}) {
103
+ const mode = (typeof env === 'object' ? env?.mode : undefined) ?? 'production';
93
104
  return {
94
105
  name: '@quilted/app/server',
95
106
  async options(originalOptions) {
@@ -100,10 +111,62 @@ function quiltAppServer(options = {}) {
100
111
  };
101
112
  const [{
102
113
  magicModuleRequestRouterEntry
103
- }] = await Promise.all([import('./request-router.esnext')]);
114
+ }, {
115
+ sourceCode
116
+ }, nodePlugins] = await Promise.all([import('./request-router.esnext'), import('./shared/source-code.esnext'), getNodePlugins()]);
117
+ newPlugins.push(...nodePlugins);
118
+ newPlugins.push(sourceCode({
119
+ mode
120
+ }));
121
+ if (env) {
122
+ const {
123
+ magicModuleEnv,
124
+ replaceProcessEnv
125
+ } = await import('./env.esnext');
126
+ if (typeof env === 'boolean') {
127
+ newPlugins.push(replaceProcessEnv({
128
+ mode
129
+ }));
130
+ newPlugins.push(magicModuleEnv({
131
+ mode
132
+ }));
133
+ } else {
134
+ newPlugins.push(replaceProcessEnv({
135
+ mode
136
+ }));
137
+ newPlugins.push(magicModuleEnv({
138
+ mode,
139
+ ...env
140
+ }));
141
+ }
142
+ }
143
+ if (app) {
144
+ newPlugins.push(magicModuleAppComponent({
145
+ entry: app
146
+ }));
147
+ }
104
148
  newPlugins.push(magicModuleRequestRouterEntry());
105
- newPlugins.push(magicModuleAppRequestRouter(options));
149
+ newPlugins.push(magicModuleAppRequestRouter({
150
+ entry
151
+ }));
152
+ if (graphql) {
153
+ const {
154
+ graphql
155
+ } = await import('./graphql.esnext');
156
+ newPlugins.push(graphql({
157
+ manifest: false
158
+ }));
159
+ }
106
160
  return newOptions;
161
+ },
162
+ outputOptions(originalOptions) {
163
+ return {
164
+ ...originalOptions,
165
+ // format,
166
+ format: 'esm',
167
+ dir: path.resolve(`build/server`),
168
+ entryFileNames: 'server.js'
169
+ };
107
170
  }
108
171
  };
109
172
  }
@@ -177,5 +240,71 @@ function magicModuleAppBrowserEntry({
177
240
  }
178
241
  });
179
242
  }
243
+ const FRAMEWORK_CHUNK_NAME = 'framework';
244
+ const POLYFILLS_CHUNK_NAME = 'polyfills';
245
+ const VENDOR_CHUNK_NAME = 'vendor';
246
+ const INTERNALS_CHUNK_NAME = 'internals';
247
+ const SHARED_CHUNK_NAME = 'shared';
248
+ const PACKAGES_CHUNK_NAME = 'packages';
249
+ const GLOBAL_CHUNK_NAME = 'global';
250
+ const FRAMEWORK_TEST_STRINGS = ['/node_modules/preact/', '/node_modules/react/', '/node_modules/js-cookie/', '/node_modules/@quilted/quilt/', '/node_modules/@preact/signals/', '/node_modules/@preact/signals-core/',
251
+ // TODO I should turn this into an allowlist
252
+ /node_modules[/]@quilted[/](?!react-query|swr)/];
253
+ const POLYFILL_TEST_STRINGS = ['/node_modules/@quilted/polyfills/', '/node_modules/core-js/', '/node_modules/whatwg-fetch/', '/node_modules/regenerator-runtime/', '/node_modules/abort-controller/'];
254
+ const INTERNALS_TEST_STRINGS = ['\x00commonjsHelpers.js', '/node_modules/@babel/runtime/'];
255
+
256
+ // When building from source, quilt packages are not in node_modules,
257
+ // so we instead add their repo paths to the list of framework test strings.
258
+ if (process.env.QUILT_FROM_SOURCE) {
259
+ FRAMEWORK_TEST_STRINGS.push('/quilt/packages/');
260
+ }
261
+
262
+ // Inspired by Vite: https://github.com/vitejs/vite/blob/c69f83615292953d40f07b1178d1ed1d72abe695/packages/vite/source/node/build.ts#L567
263
+ function createManualChunksSorter() {
264
+ // TODO: make this more configurable, and make it so that we bundle more intelligently
265
+ // for split entries
266
+ const packagesPath = path.resolve('packages') + path.sep;
267
+ const globalPath = path.resolve('global') + path.sep;
268
+ const sharedPath = path.resolve('shared') + path.sep;
269
+ return (id, {
270
+ getModuleInfo
271
+ }) => {
272
+ if (INTERNALS_TEST_STRINGS.some(test => id.includes(test))) {
273
+ return INTERNALS_CHUNK_NAME;
274
+ }
275
+ if (FRAMEWORK_TEST_STRINGS.some(test => typeof test === 'string' ? id.includes(test) : test.test(id))) {
276
+ return FRAMEWORK_CHUNK_NAME;
277
+ }
278
+ if (POLYFILL_TEST_STRINGS.some(test => id.includes(test))) {
279
+ return POLYFILLS_CHUNK_NAME;
280
+ }
281
+ let bundleBaseName;
282
+ let relativeId;
283
+ if (id.includes('/node_modules/')) {
284
+ const moduleInfo = getModuleInfo(id);
285
+
286
+ // If the only dependency is another vendor, let Rollup handle the naming
287
+ if (moduleInfo == null) return;
288
+ if (moduleInfo.importers.length > 0 && moduleInfo.importers.every(importer => importer.includes('/node_modules/'))) {
289
+ return;
290
+ }
291
+ bundleBaseName = VENDOR_CHUNK_NAME;
292
+ relativeId = id.replace(/^.*[/]node_modules[/]/, '');
293
+ } else if (id.startsWith(packagesPath)) {
294
+ bundleBaseName = PACKAGES_CHUNK_NAME;
295
+ relativeId = id.replace(packagesPath, '');
296
+ } else if (id.startsWith(globalPath)) {
297
+ bundleBaseName = GLOBAL_CHUNK_NAME;
298
+ relativeId = id.replace(globalPath, '');
299
+ } else if (id.startsWith(sharedPath)) {
300
+ bundleBaseName = SHARED_CHUNK_NAME;
301
+ relativeId = id.replace(sharedPath, '');
302
+ }
303
+ if (bundleBaseName == null || relativeId == null) {
304
+ return;
305
+ }
306
+ return `${bundleBaseName}-${relativeId.split(path.sep)[0]?.split('.')[0]}`;
307
+ };
308
+ }
180
309
 
181
- export { magicModuleAppBrowserEntry, magicModuleAppComponent, magicModuleAppRequestRouter, quiltApp, quiltAppBrowser, quiltAppServer };
310
+ export { magicModuleAppBrowserEntry, magicModuleAppComponent, magicModuleAppRequestRouter, quiltAppBrowser, quiltAppServer };
@@ -0,0 +1,181 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { print, parse } from 'graphql';
3
+
4
+ const IMPORT_REGEX = /^#import\s+['"]([^'"]*)['"];?[\s\n]*/gm;
5
+ const DEFAULT_NAME = 'Operation';
6
+ function cleanGraphQLDocument(document, {
7
+ removeUnused = true
8
+ } = {}) {
9
+ if (removeUnused) {
10
+ removeUnusedDefinitions(document, {
11
+ exclude: removeUnused === true ? new Set() : removeUnused.exclude
12
+ });
13
+ }
14
+ for (const definition of document.definitions) {
15
+ addTypename(definition);
16
+ }
17
+ const normalizedSource = minifyGraphQLSource(print(document));
18
+ const normalizedDocument = parse(normalizedSource);
19
+ for (const definition of normalizedDocument.definitions) {
20
+ stripLoc(definition);
21
+ }
22
+
23
+ // This ID is a hash of the full file contents that are part of the document,
24
+ // including other documents that are injected in, but excluding any unused
25
+ // fragments. This is useful for things like persisted queries.
26
+ const id = createHash('sha256').update(normalizedSource).digest('hex');
27
+ Reflect.defineProperty(normalizedDocument, 'id', {
28
+ value: id,
29
+ enumerable: true,
30
+ writable: false,
31
+ configurable: false
32
+ });
33
+ Reflect.defineProperty(normalizedDocument, 'loc', {
34
+ value: stripDocumentLoc(normalizedDocument.loc),
35
+ enumerable: true,
36
+ writable: false,
37
+ configurable: false
38
+ });
39
+ return normalizedDocument;
40
+ }
41
+ function extractGraphQLImports(rawSource) {
42
+ const imports = new Set();
43
+ const source = rawSource.replace(IMPORT_REGEX, (_, imported) => {
44
+ imports.add(imported);
45
+ return '';
46
+ });
47
+ return {
48
+ imports: [...imports],
49
+ source
50
+ };
51
+ }
52
+ function toGraphQLOperation(documentOrSource) {
53
+ const document = typeof documentOrSource === 'string' ? cleanGraphQLDocument(parse(documentOrSource)) : documentOrSource;
54
+ return {
55
+ id: document.id,
56
+ name: operationNameForDocument(document),
57
+ source: document.loc.source.body
58
+ };
59
+ }
60
+ function operationNameForDocument(document) {
61
+ return document.definitions.find(definition => definition.kind === 'OperationDefinition')?.name?.value;
62
+ }
63
+ function removeUnusedDefinitions(document, {
64
+ exclude
65
+ }) {
66
+ const usedDefinitions = new Set();
67
+ const dependencies = definitionDependencies(document.definitions);
68
+ const markAsUsed = definition => {
69
+ if (usedDefinitions.has(definition)) {
70
+ return;
71
+ }
72
+ usedDefinitions.add(definition);
73
+ for (const dependency of dependencies.get(definition) || []) {
74
+ markAsUsed(dependency);
75
+ }
76
+ };
77
+ for (const definition of document.definitions) {
78
+ if (definition.kind === 'FragmentDefinition') {
79
+ if (exclude.has(definition.name.value)) {
80
+ markAsUsed(definition);
81
+ }
82
+ } else {
83
+ markAsUsed(definition);
84
+ }
85
+ }
86
+ document.definitions = [...usedDefinitions];
87
+ }
88
+ function definitionDependencies(definitions) {
89
+ const executableDefinitions = definitions.filter(definition => definition.kind === 'OperationDefinition' || definition.kind === 'FragmentDefinition');
90
+ const definitionsByName = new Map(executableDefinitions.map(definition => [definition.name ? definition.name.value : DEFAULT_NAME, definition]));
91
+ return new Map(executableDefinitions.map(executableNode => [executableNode, [...collectUsedFragmentSpreads(executableNode, new Set())].map(usedFragment => {
92
+ const definition = definitionsByName.get(usedFragment);
93
+ if (definition == null) {
94
+ throw new Error(`You attempted to use the fragment '${usedFragment}' (in '${executableNode.name ? executableNode.name.value : DEFAULT_NAME}'), but it does not exist. Maybe you forgot to import it from another document?`);
95
+ }
96
+ return definition;
97
+ })]));
98
+ }
99
+ const TYPENAME_FIELD = {
100
+ kind: 'Field',
101
+ alias: null,
102
+ name: {
103
+ kind: 'Name',
104
+ value: '__typename'
105
+ }
106
+ };
107
+ function addTypename(definition) {
108
+ for (const {
109
+ selections
110
+ } of selectionSetsForDefinition(definition)) {
111
+ const hasTypename = selections.some(selection => selection.kind === 'Field' && selection.name.value === '__typename');
112
+ if (!hasTypename) {
113
+ selections.push(TYPENAME_FIELD);
114
+ }
115
+ }
116
+ }
117
+ function collectUsedFragmentSpreads(definition, usedSpreads) {
118
+ for (const selection of selectionsForDefinition(definition)) {
119
+ if (selection.kind === 'FragmentSpread') {
120
+ usedSpreads.add(selection.name.value);
121
+ }
122
+ }
123
+ return usedSpreads;
124
+ }
125
+ function selectionsForDefinition(definition) {
126
+ if (!('selectionSet' in definition) || definition.selectionSet == null) {
127
+ return [][Symbol.iterator]();
128
+ }
129
+ return selectionsForSelectionSet(definition.selectionSet);
130
+ }
131
+ function* selectionSetsForDefinition(definition) {
132
+ if (!('selectionSet' in definition) || definition.selectionSet == null) {
133
+ return [][Symbol.iterator]();
134
+ }
135
+ if (definition.kind !== 'OperationDefinition') {
136
+ yield definition.selectionSet;
137
+ }
138
+ for (const nestedSelection of selectionsForDefinition(definition)) {
139
+ if ('selectionSet' in nestedSelection && nestedSelection.selectionSet != null) {
140
+ yield nestedSelection.selectionSet;
141
+ }
142
+ }
143
+ }
144
+ function* selectionsForSelectionSet({
145
+ selections
146
+ }) {
147
+ for (const selection of selections) {
148
+ yield selection;
149
+ if ('selectionSet' in selection && selection.selectionSet != null) {
150
+ yield* selectionsForSelectionSet(selection.selectionSet);
151
+ }
152
+ }
153
+ }
154
+ function stripDocumentLoc(loc) {
155
+ const normalizedLoc = {
156
+ ...loc
157
+ };
158
+ delete normalizedLoc.endToken;
159
+ delete normalizedLoc.startToken;
160
+ return normalizedLoc;
161
+ }
162
+ function stripLoc(value) {
163
+ if (Array.isArray(value)) {
164
+ value.forEach(stripLoc);
165
+ } else if (typeof value === 'object') {
166
+ if (value == null) {
167
+ return;
168
+ }
169
+ if ('loc' in value) {
170
+ delete value.loc;
171
+ }
172
+ for (const key of Object.keys(value)) {
173
+ stripLoc(value[key]);
174
+ }
175
+ }
176
+ }
177
+ function minifyGraphQLSource(source) {
178
+ return source.replace(/#.*/g, '').replace(/\\n/g, ' ').replace(/\s\s+/g, ' ').replace(/\s*({|}|\(|\)|\.|:|,)\s*/g, '$1');
179
+ }
180
+
181
+ export { cleanGraphQLDocument, extractGraphQLImports, minifyGraphQLSource, toGraphQLOperation };
@@ -0,0 +1,84 @@
1
+ import { dirname } from 'node:path';
2
+ import { mkdir, writeFile, readFile } from 'node:fs/promises';
3
+ import { parse } from 'graphql';
4
+ import { toGraphQLOperation, cleanGraphQLDocument, extractGraphQLImports } from './graphql/transform.esnext';
5
+
6
+ function graphql({
7
+ manifest
8
+ } = {}) {
9
+ const shouldWriteManifest = Boolean(manifest);
10
+ const manifestPath = typeof manifest === 'string' ? manifest : `manifests/graphql.json`;
11
+ return {
12
+ name: '@quilted/graphql',
13
+ async transform(code, id) {
14
+ if (!id.endsWith('.graphql') && !id.endsWith('.gql')) return null;
15
+ const topLevelDefinitions = new Set();
16
+ const loadedDocument = await loadDocument(code, id, this, (document, level) => {
17
+ if (level !== 0) return;
18
+ for (const definition of document.definitions) {
19
+ if ('name' in definition && definition.name != null) {
20
+ topLevelDefinitions.add(definition.name.value);
21
+ }
22
+ }
23
+ });
24
+ const document = toGraphQLOperation(cleanGraphQLDocument(loadedDocument, {
25
+ removeUnused: {
26
+ exclude: topLevelDefinitions
27
+ }
28
+ }));
29
+ return {
30
+ code: `export default JSON.parse(${JSON.stringify(JSON.stringify(document))})`,
31
+ meta: shouldWriteManifest ? {
32
+ quilt: {
33
+ graphql: document
34
+ }
35
+ } : undefined
36
+ };
37
+ },
38
+ async generateBundle() {
39
+ if (!shouldWriteManifest) return;
40
+ const operations = {};
41
+ for (const moduleId of this.getModuleIds()) {
42
+ const operation = this.getModuleInfo(moduleId)?.meta?.quilt?.graphql;
43
+ if (operation != null && typeof operation.id === 'string' && typeof operation.source === 'string') {
44
+ operations[operation.id] = operation.source;
45
+ }
46
+ }
47
+ await mkdir(dirname(manifestPath), {
48
+ recursive: true
49
+ });
50
+ await writeFile(manifestPath, JSON.stringify(operations, null, 2));
51
+ }
52
+ };
53
+ }
54
+ async function loadDocument(code, file, plugin, add, level = 0, seen = new Set()) {
55
+ const {
56
+ imports,
57
+ source
58
+ } = extractGraphQLImports(code);
59
+ const document = parse(source);
60
+ add?.(document, level);
61
+ if (imports.length === 0) {
62
+ return document;
63
+ }
64
+ const resolvedImports = await Promise.all(imports.map(async imported => {
65
+ if (seen.has(imported)) return;
66
+ seen.add(imported);
67
+ const resolvedId = await plugin.resolve(imported, file);
68
+ if (resolvedId == null) {
69
+ throw new Error(`Could not find ${JSON.stringify(imported)} from ${JSON.stringify(file)}`);
70
+ }
71
+ plugin.addWatchFile(resolvedId.id);
72
+ const contents = await readFile(resolvedId.id, {
73
+ encoding: 'utf8'
74
+ });
75
+ return loadDocument(contents, resolvedId.id, plugin, add, level + 1, seen);
76
+ }));
77
+ for (const importedDocument of resolvedImports) {
78
+ if (importedDocument == null) continue;
79
+ document.definitions.push(...importedDocument.definitions);
80
+ }
81
+ return document;
82
+ }
83
+
84
+ export { graphql };
@@ -1,3 +1,3 @@
1
1
  export { magicModuleEnv } from './env.esnext';
2
- export { magicModuleAppBrowserEntry, magicModuleAppComponent, magicModuleAppRequestRouter, quiltApp, quiltAppBrowser, quiltAppServer } from './app.esnext';
2
+ export { magicModuleAppBrowserEntry, magicModuleAppComponent, magicModuleAppRequestRouter, quiltAppBrowser, quiltAppServer } from './app.esnext';
3
3
  export { magicModuleRequestRouterEntry } from './request-router.esnext';
@@ -0,0 +1,38 @@
1
+ import { createRequire } from 'node:module';
2
+ import babel from '@rollup/plugin-babel';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ function sourceCode({
6
+ mode,
7
+ targets
8
+ }) {
9
+ return babel({
10
+ configFile: false,
11
+ babelrc: false,
12
+ presets: [require.resolve('@babel/preset-typescript'), [require.resolve('@babel/preset-react'), {
13
+ runtime: 'automatic',
14
+ importSource: 'react',
15
+ development: mode === 'development'
16
+ }], [require.resolve('@babel/preset-env'), {
17
+ // @ts-expect-error This is a valid option
18
+ corejs: '3.15',
19
+ useBuiltIns: 'usage',
20
+ bugfixes: true,
21
+ shippedProposals: true,
22
+ // I thought I wanted this on, but if you do this, Babel
23
+ // stops respecting the top-level `targets` option and tries
24
+ // to use the targets passed to the preset directly instead.
25
+ ignoreBrowserslistConfig: true
26
+ }]],
27
+ plugins: [[require.resolve('@babel/plugin-proposal-decorators'), {
28
+ version: '2023-01'
29
+ }]],
30
+ targets,
31
+ extensions: ['.ts', '.tsx', '.mts', '.mtsx', '.js', '.jsx', '.es6', '.es', '.mjs'],
32
+ exclude: 'node_modules/**',
33
+ babelHelpers: 'bundled',
34
+ skipPreflightCheck: true
35
+ });
36
+ }
37
+
38
+ export { sourceCode };