@kaliber/build 0.0.115 → 0.0.119

Sign up to get free protection for your applications and to get access to all the features.
package/.eslintrc CHANGED
@@ -1,7 +1,7 @@
1
1
  // Based on eslint-config-react-app; https://github.com/facebookincubator/create-react-app/blob/cd3d04b71e91f533bdbdc3856775e1da81d445cf/packages/eslint-config-react-app/index.js
2
2
 
3
3
  {
4
- "parser": "babel-eslint",
4
+ "parser": "@babel/eslint-parser",
5
5
  "plugins": ["import", "jsx-a11y", "react", "react-hooks", "@kaliber/eslint-plugin"],
6
6
  "root": true,
7
7
  "env": {
@@ -30,6 +30,10 @@
30
30
  "jsx": true,
31
31
  "generators": true,
32
32
  "experimentalObjectRestSpread": true
33
+ },
34
+ "requireConfigFile": false,
35
+ "babelOptions": {
36
+ "presets": ["@babel/preset-react"]
33
37
  }
34
38
  },
35
39
 
@@ -46,6 +50,7 @@
46
50
  "@kaliber/naming-policy": "warn",
47
51
  "@kaliber/no-default-export": "warn",
48
52
  "@kaliber/no-relative-parent-import": "warn",
53
+ "@kaliber/jsx-key": "warn",
49
54
 
50
55
  "brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
51
56
  "indent": ["warn", 2, {
@@ -229,7 +234,7 @@
229
234
  "react/jsx-equals-spacing": "warn",
230
235
  "react/jsx-indent": ["warn", 2],
231
236
  "react/jsx-indent-props": ["warn", 2],
232
- "react/jsx-key": "warn",
237
+ "react/jsx-key": "off",
233
238
  "react/jsx-no-comment-textnodes": "warn",
234
239
  "react/jsx-no-duplicate-props": ["warn", { "ignoreCase": true }],
235
240
  "react/jsx-no-target-blank": "warn",
package/lib/build.js CHANGED
@@ -52,7 +52,7 @@ const { kaliber: { compileWithBabel: userDefinedcompileWithBabel = [], publicPat
52
52
 
53
53
  const recognizedTemplates = Object.keys(templateRenderers)
54
54
 
55
- const kaliberBuildClientModules = [/(@kaliber\/build\/lib\/(stylesheet|javascript|polyfill|withPolyfill|hot-module-replacement-client|rollbar)|ansi-regex)/]
55
+ const kaliberBuildClientModules = [/(@kaliber\/build\/lib\/(stylesheet|javascript|polyfill|withPolyfill|hot-module-replacement-client|rollbar|universalComponents)|ansi-regex)/]
56
56
  const compileWithBabel = kaliberBuildClientModules.concat(userDefinedcompileWithBabel)
57
57
 
58
58
  const babelLoader = {
package/lib/serve.js CHANGED
@@ -4,6 +4,7 @@ const helmet = require('helmet')
4
4
  const { access } = require('fs')
5
5
  const { parsePath } = require('history')
6
6
  const { resolve } = require('path')
7
+ const morgan = require('morgan')
7
8
 
8
9
  const templateRenderers = require('./getTemplateRenderers')
9
10
 
@@ -30,6 +31,7 @@ const isProduction = process.env.NODE_ENV === 'production'
30
31
 
31
32
  const notCached = ['html', 'txt', 'json', 'xml']
32
33
 
34
+ if (isProduction) app.use(morgan('combined'))
33
35
  // hsts-headers are sent by our loadbalancer
34
36
  app.use(helmet(Object.assign({ hsts: false, contentSecurityPolicy: false }, helmetOptions)))
35
37
  app.use(compression())
@@ -0,0 +1,144 @@
1
+ import ReactDom from 'react-dom'
2
+
3
+ const containerMarker = 'data-kaliber-component-container'
4
+
5
+ export function ComponentServerWrapper({ componentName, props, renderedComponent }) {
6
+ const componentInfo = JSON.stringify({ componentName, props })
7
+ return (
8
+ <>
9
+ {/* It is not possible to render the html of a React-rendered component without a container
10
+ because dangerouslySetInnerHTML is the only route to get raw html into the resulting html */}
11
+ <kaliber-component-container dangerouslySetInnerHTML={{ __html: renderedComponent }} />
12
+
13
+ {/* Use render blocking script to remove the container and supply the correct comment nodes.
14
+ This ensures the page is never rendered with the intermediate structure */}
15
+ <script dangerouslySetInnerHTML={{ __html: restructureDomNodes(componentInfo) }} />
16
+ </>
17
+ )
18
+ }
19
+
20
+ export function findComponents({ componentName }) {
21
+ if (typeof window === 'undefined') throw new Error(`The function 'findComponents' can only be used in the browser`)
22
+
23
+ const findComponentCache = getFindComponentCache()
24
+ const components = findComponentCache[componentName] || []
25
+ return components
26
+
27
+ function getFindComponentCache() {
28
+ if (!findComponents.cache) findComponents.cache = findAndGroupAllComponents()
29
+ return findComponents.cache
30
+
31
+ function findAndGroupAllComponents() {
32
+ return groupComponentsByName(findAllComponents())
33
+ }
34
+ }
35
+ }
36
+
37
+ export function hydrate(
38
+ component,
39
+ {
40
+ nodes,
41
+ endNode: insertBefore,
42
+ container = createContainer({ eventTarget: insertBefore.parentNode }),
43
+ },
44
+ ) {
45
+ // Move the rendered nodes to a container before hydrating
46
+ nodes.forEach((x) => { container.appendChild(x) })
47
+
48
+ ReactDom.hydrate(component, container)
49
+
50
+ // Capture the rendered nodes before they are moved by inserting the container
51
+ const renderedNodes = Array.from(container.childNodes)
52
+ insertBefore.parentNode.insertBefore(container, insertBefore)
53
+
54
+ return { container, renderedNodes }
55
+ }
56
+
57
+ function createContainer({ eventTarget }) {
58
+ // React attaches event listeners to the container on hydrate or render. This does not make
59
+ // sense for document fragments, so we forward all EventTarget methods.
60
+ const container = document.createDocumentFragment()
61
+ container.addEventListener = (...args) => eventTarget.addEventListener(...args)
62
+ container.removeEventListener = (...args) => eventTarget.removeEventListener(...args)
63
+ container.dispatchEvent = (...args) => eventTarget.dispatchEvent(...args)
64
+ return container
65
+ }
66
+
67
+ function findAllComponents() {
68
+ const containers = document.querySelectorAll(`*[${containerMarker}]`)
69
+ return Array.from(containers).flatMap(extractServerRenderedComponents) // this requires flatMap polyfill (es2019)
70
+ }
71
+
72
+ function groupComponentsByName(allComponents) {
73
+ return allComponents.reduce(
74
+ (result, { info: { componentName, props }, nodes, endNode }) => {
75
+ const components = result[componentName] || (result[componentName] = [])
76
+ components.push({ componentName, nodes, endNode, props })
77
+ return result
78
+ },
79
+ {}
80
+ )
81
+ }
82
+
83
+ function restructureDomNodes(componentInfo) {
84
+ return `|var d=document,s=d.currentScript,p=s.parentNode,c=s.previousSibling;
85
+ |p.setAttribute('${containerMarker}',''); // set marker on container so we can retrieve nodes that contain components
86
+ |p.replaceChild(d.createComment('start'),c); // replace kaliber-component-container element with a 'start' comment
87
+ |p.insertBefore(d.createComment(JSON.stringify(${componentInfo})),s); // create a comment containing the component info
88
+ |Array.from(c.childNodes).forEach(x=>{p.insertBefore(x,s)}); // insert all children from the kaliber-component-container element
89
+ |p.replaceChild(d.createComment('end'),s); // create an 'end' comment
90
+ |`.replace(/^\s*\|/gm, '').replace(/\s*\/\/[^;]*?$/gm, '').replace(/\n/g, '')
91
+ }
92
+
93
+ function extractServerRenderedComponents(container) {
94
+ // These steps work with the DOM structure created by the render blocking script
95
+ const steps = [
96
+ [not(isStart), ignore, repeat],
97
+ [isStart, ignore, nextStep],
98
+ [isComment, dataAsJson('info'), nextStep],
99
+ [not(isEnd), addNodeToCollection('nodes'), repeat],
100
+ [isEnd, addNode('endNode'), commitAndRestart]
101
+ ]
102
+
103
+ return executeSteps({ steps, node: container.firstChild })
104
+ }
105
+
106
+ function executeSteps({ steps, node, data = {}, set = [], originalSteps = steps }) {
107
+ if (!steps.length || !node) return set
108
+
109
+ const [[predicate, extractData, determineNext]] = steps
110
+
111
+ return executeSteps(
112
+ predicate(node)
113
+ ? determineNext({ node, steps, data: extractData({ data, node }), set, originalSteps })
114
+ : tryNextStep({ node, steps, data, set, originalSteps })
115
+ )
116
+ }
117
+
118
+ // Predicates
119
+ function isStart(x) { return isComment(x) && x.data === 'start' }
120
+ function isEnd(x) { return isComment(x) && x.data === 'end' }
121
+ function isComment(x) { return x.nodeType === 8 }
122
+ function not(f) { return x => !f(x) }
123
+
124
+ // Extraction
125
+ function ignore({ data }) { return data }
126
+ function dataAsJson(key) { return ({ data, node }) => ({ ...data, [key]: JSON.parse(node.data) }) }
127
+ function addNodeToCollection(key) {
128
+ return ({ data, node }) => ({ ...data, [key]: (data[key] ?? []).concat(node) })
129
+ }
130
+ function addNode(key) { return ({ data, node }) => ({ ...data, [key]: node }) }
131
+
132
+ // Control
133
+ function repeat({ node, ...state }) {
134
+ return { node: node.nextSibling, ...state }
135
+ }
136
+ function nextStep({ node, steps, ...state }) {
137
+ return { node: node.nextSibling, steps: steps.slice(1), ...state }
138
+ }
139
+ function tryNextStep({ steps, ...state }) {
140
+ return { steps: steps.slice(1), ...state }
141
+ }
142
+ function commitAndRestart({ node, originalSteps, data, set }) {
143
+ return { node: node.nextSibling, steps: originalSteps, data: {}, set: set.concat(data) }
144
+ }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.115",
2
+ "version": "0.0.119",
3
3
  "name": "@kaliber/build",
4
4
  "description": "Zero configuration, opinionated webpack / react build setup",
5
5
  "scripts": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@babel/core": "^7.11.6",
19
+ "@babel/eslint-parser": "^7.16.5",
19
20
  "@babel/plugin-proposal-class-properties": "^7.10.4",
20
21
  "@babel/plugin-proposal-decorators": "^7.10.5",
21
22
  "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
@@ -24,12 +25,11 @@
24
25
  "@babel/plugin-transform-async-to-generator": "^7.10.4",
25
26
  "@babel/plugin-transform-runtime": "^7.11.5",
26
27
  "@babel/preset-env": "^7.11.5",
27
- "@babel/preset-react": "^7.10.4",
28
+ "@babel/preset-react": "^7.16.7",
28
29
  "@babel/runtime": "^7.11.2",
29
30
  "@kaliber/config": "^0.0.8",
30
31
  "@kaliber/eslint-plugin": "*",
31
32
  "ansi-regex": "^5.0.0",
32
- "babel-eslint": "^10.1.0",
33
33
  "babel-loader": "^8.0.6",
34
34
  "cache-loader": "^4.1.0",
35
35
  "classnames": "^2.2.6",
@@ -62,6 +62,7 @@
62
62
  "import-fresh": "^3.2.1",
63
63
  "json-loader": "^0.5.7",
64
64
  "loader-utils": "^2.0.0",
65
+ "morgan": "^1.10.0",
65
66
  "npm-run-all": "^4.1.5",
66
67
  "pkg-dir": "^5.0.0",
67
68
  "postcss": "^7.0.34",
@@ -1,9 +1,12 @@
1
1
  module.exports = {
2
2
  flexChildProps: [
3
- 'flex', 'flex-grow', 'flex-shrink', 'flex-basis', 'order',
3
+ 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
4
4
  ],
5
5
  gridChildProps: [
6
6
  'grid', 'grid-area', 'grid-column', 'grid-row',
7
7
  'grid-column-start', 'grid-column-end', 'grid-row-start', 'grid-row-end',
8
8
  ],
9
+ flexOrGridChildProps: [
10
+ 'order'
11
+ ],
9
12
  }
@@ -31,6 +31,7 @@ function checkRuleRelation({ rule, triggerProperties, rulesToCheck, requiredProp
31
31
  if (!invalidDecl) return { result: 'missing', prop }
32
32
  if (!expectedValue) return
33
33
  const { value } = invalidDecl
34
+ if (Array.isArray(expectedValue) && expectedValue.includes(value)) return
34
35
  if (value === expectedValue) return
35
36
  return { result: 'invalid', prop, invalidDecl, value, expectedValue }
36
37
  }
@@ -23,7 +23,11 @@ const messages = {
23
23
 
24
24
  module.exports = {
25
25
  ruleName: 'css-global',
26
- ruleInteraction: null,
26
+ ruleInteraction: {
27
+ 'layout-related-properties': {
28
+ childAllowDecl: decl => isCustomProperty(decl)
29
+ },
30
+ },
27
31
  cssRequirements: null,
28
32
  messages,
29
33
  create(config) {
@@ -35,6 +39,7 @@ module.exports = {
35
39
  }
36
40
 
37
41
  function isInCssGlobal(root) { return matchesFile(root, filename => filename.includes('/cssGlobal/')) }
42
+ function isCustomProperty({ prop }) { return prop.startsWith('--') }
38
43
 
39
44
  function checkAtRules({ originalRoot, report }) {
40
45
  const inCssGlobal = isInCssGlobal(originalRoot)
@@ -85,4 +85,19 @@ test('css-global', {
85
85
  },
86
86
  ],
87
87
  },
88
+ 'layout-related-properties': {
89
+ valid: [
90
+ {
91
+ title: 'valid - allow custom properties in child selectors',
92
+ code: `
93
+ .parent {
94
+ & > .child{
95
+ --x: 0;
96
+ }
97
+ }
98
+ `,
99
+ },
100
+ ],
101
+ invalid: [],
102
+ }
88
103
  })
@@ -6,7 +6,7 @@ const {
6
6
  getRootRules,
7
7
  } = require('../../machinery/ast')
8
8
  const { checkRuleRelation } = require('../../machinery/relations')
9
- const { flexChildProps, gridChildProps } = require('../../machinery/css')
9
+ const { flexChildProps, gridChildProps, flexOrGridChildProps } = require('../../machinery/css')
10
10
 
11
11
  const messages = {
12
12
  'nested - missing stacking context in parent':
@@ -21,12 +21,17 @@ const messages = {
21
21
  `missing \`display: flex;\`\n` +
22
22
  `\`${prop}\` can only be used when the containing root rule has \`display: flex;\` - ` +
23
23
  `add \`display: flex;\` to the containing root rule or, if this is caused by a media query ` +
24
- `that overrides \`display: flex;\`, use \`flex: unset\``,
24
+ `that overrides \`display: flex;\`, use \`${prop}: unset\``,
25
25
  'nested - require display grid in parent': prop =>
26
26
  `missing \`display: grid;\`\n` +
27
27
  `\`${prop}\` can only be used when the containing root rule has \`display: grid;\` - ` +
28
28
  `add \`display: grid;\` to the containing root rule or, if this is caused by a media query ` +
29
- `that overrides \`display: grid;\`, use \`grid: unset\``,
29
+ `that overrides \`display: grid;\`, use \`${prop}: unset\``,
30
+ 'nested - require display flex or grid in parent': prop =>
31
+ `missing \`display: flex;\` or \`display: grid;\`\n` +
32
+ `\`${prop}\` can only be used when the containing root rule has \`display: flex;\` or \`display: grid;\` - ` +
33
+ `add \`display: flex;\` or \`display: grid;\` to the containing root rule or, if this is caused by a media query ` +
34
+ `that overrides \`display: flex;\` or \`display: grid;\`, use \`${prop}: unset\``,
30
35
  'invalid pointer events':
31
36
  `Incorrect pointer events combination\n` +
32
37
  `you can only set pointer events in a child if the parent disables pointer events - ` +
@@ -58,18 +63,24 @@ const childParentRelations = {
58
63
  ['position', 'relative']
59
64
  ]
60
65
  },
61
- rootHasPositionFlex: {
66
+ rootHasDisplayFlex: {
62
67
  nestedHasOneOf: flexChildProps,
63
68
  requireInRoot: [
64
69
  ['display', 'flex']
65
70
  ]
66
71
  },
67
- rootHasPositionGrid: {
72
+ rootHasDisplayGrid: {
68
73
  nestedHasOneOf: gridChildProps,
69
74
  requireInRoot: [
70
75
  ['display', 'grid']
71
76
  ]
72
77
  },
78
+ rootHasDisplayFlexOrGrid: {
79
+ nestedHasOneOf: flexOrGridChildProps,
80
+ requireInRoot: [
81
+ ['display', ['flex', 'grid']]
82
+ ]
83
+ },
73
84
  validPointerEvents: {
74
85
  nestedHasOneOf: [
75
86
  ['pointer-events', 'auto']
@@ -109,6 +120,7 @@ module.exports = {
109
120
  absoluteHasRelativeParent({ root: modifiedRoot, report })
110
121
  requireDisplayFlexInParent({ root: modifiedRoot, report })
111
122
  requireDisplayGridInParent({ root: modifiedRoot, report })
123
+ requireDisplayFlexOrGridInParent({ root: modifiedRoot, report })
112
124
  validPointerEvents({ root: modifiedRoot, report })
113
125
  relativeToParent({ root: modifiedRoot, report })
114
126
  }
@@ -137,7 +149,7 @@ function absoluteHasRelativeParent({ root, report }) {
137
149
 
138
150
  function requireDisplayFlexInParent({ root, report }) {
139
151
  withNestedRules(root, (rule, parent) => {
140
- const result = checkChildParentRelation(rule, childParentRelations.rootHasPositionFlex)
152
+ const result = checkChildParentRelation(rule, childParentRelations.rootHasDisplayFlex)
141
153
 
142
154
  result.forEach(({ result, prop, triggerDecl, rootDecl, value, expectedValue }) => {
143
155
  report(triggerDecl, messages['nested - require display flex in parent'](triggerDecl.prop))
@@ -147,7 +159,7 @@ function requireDisplayFlexInParent({ root, report }) {
147
159
 
148
160
  function requireDisplayGridInParent({ root, report }) {
149
161
  withNestedRules(root, (rule, parent) => {
150
- const result = checkChildParentRelation(rule, childParentRelations.rootHasPositionGrid)
162
+ const result = checkChildParentRelation(rule, childParentRelations.rootHasDisplayGrid)
151
163
 
152
164
  result.forEach(({ result, prop, triggerDecl, rootDecl, value, expectedValue }) => {
153
165
  report(triggerDecl, messages['nested - require display grid in parent'](triggerDecl.prop))
@@ -155,6 +167,16 @@ function requireDisplayGridInParent({ root, report }) {
155
167
  })
156
168
  }
157
169
 
170
+ function requireDisplayFlexOrGridInParent({ root, report }) {
171
+ withNestedRules(root, (rule, parent) => {
172
+ const result = checkChildParentRelation(rule, childParentRelations.rootHasDisplayFlexOrGrid)
173
+
174
+ result.forEach(({ result, prop, triggerDecl, rootDecl, value, expectedValue }) => {
175
+ report(triggerDecl, messages['nested - require display flex or grid in parent'](triggerDecl.prop))
176
+ })
177
+ })
178
+ }
179
+
158
180
  function validPointerEvents({ root, report }) {
159
181
  withNestedRules(root, (rule, parent) => {
160
182
  const result = checkChildParentRelation(rule, childParentRelations.validPointerEvents)
@@ -29,7 +29,7 @@ test('parent-child-policy', {
29
29
  display: grid;
30
30
 
31
31
  & > .test {
32
- grid: 0; grid-area: 0; grid-column: 0; grid-row: 0;
32
+ grid: 0; grid-area: 0; grid-column: 0; grid-row: 0; order: 0;
33
33
  grid-column-start: 0; grid-column-end: 0; grid-row-start: 0; grid-row-end: 0;
34
34
  }
35
35
  }
@@ -207,12 +207,12 @@ test('parent-child-policy', {
207
207
  code: `
208
208
  .bad {
209
209
  & > .test {
210
- flex: 0; flex-grow: 0; flex-shrink: 0; flex-basis: 0; order: 0;
210
+ flex: 0; flex-grow: 0; flex-shrink: 0; flex-basis: 0;
211
211
  }
212
212
  }
213
213
  `,
214
214
  warnings: createMessages('nested - require display flex in parent', [
215
- 'flex', 'flex-grow', 'flex-shrink', 'flex-basis', 'order'
215
+ 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
216
216
  ])
217
217
  },
218
218
  {
@@ -221,13 +221,13 @@ test('parent-child-policy', {
221
221
  .bad {
222
222
  & > .test {
223
223
  @media x {
224
- flex: 0; flex-grow: 0; flex-shrink: 0; flex-basis: 0; order: 0;
224
+ flex: 0; flex-grow: 0; flex-shrink: 0; flex-basis: 0;
225
225
  }
226
226
  }
227
227
  }
228
228
  `,
229
229
  warnings: createMessages('nested - require display flex in parent', [
230
- 'flex', 'flex-grow', 'flex-shrink', 'flex-basis', 'order'
230
+ 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
231
231
  ])
232
232
  },
233
233
  {
@@ -300,6 +300,20 @@ test('parent-child-policy', {
300
300
  `,
301
301
  warnings: [messages['nested - require display flex in parent']('flex')]
302
302
  },
303
+ {
304
+ title: 'report missing flex or grid',
305
+ code: `.bad { & > .test { order: 0; } }`,
306
+ warnings: createMessages('nested - require display flex or grid in parent', [
307
+ 'order'
308
+ ])
309
+ },
310
+ {
311
+ title: '└─ take @media into account',
312
+ code: `.bad { & > .test { @media x { order: 0; } } }`,
313
+ warnings: createMessages('nested - require display flex or grid in parent', [
314
+ 'order'
315
+ ])
316
+ },
303
317
  ]
304
318
  },
305
319
  'layout-related-properties': {
@@ -0,0 +1,53 @@
1
+ const { relative } = require('path')
2
+ const { OriginalSource } = require('webpack-sources')
3
+
4
+ module.exports = ReactContainerlessUniversalClientLoader
5
+
6
+ function ReactContainerlessUniversalClientLoader(source, map, meta) {
7
+ const filename = relative(this.rootContext, this.resourcePath)
8
+ const importPath = relative(this.context, this.resourcePath)
9
+ const id = filename.replace(/[/.]/g, '_')
10
+ const wrapper = get(require('@kaliber/config'), 'kaliber.universal.clientWrapper')
11
+ const code = createClientCode({ importPath, id, wrapper })
12
+ const generated = new OriginalSource(code, this.resourcePath + '?generated').sourceAndMap()
13
+ this.callback(null, generated.source, generated.map, meta)
14
+ }
15
+
16
+ function createClientCode({ importPath, id, wrapper: wrapperPath }) {
17
+ const component = '<Component {...props} />'
18
+ const { wrapper, wrapped } = {
19
+ wrapper: wrapperPath ? `import Wrapper from '${wrapperPath}'` : '',
20
+ wrapped: wrapperPath ? `<Wrapper {...props}>${component}</Wrapper>` : component,
21
+ }
22
+
23
+ return `|import Component from './${importPath}'
24
+ |import { findComponents, hydrate, reload } from '@kaliber/build/lib/universalComponents'
25
+ |${wrapper}
26
+ |
27
+ |const components = findComponents({ componentName: '${id}' })
28
+ |let renderInfo = components.map(componentInfo => {
29
+ | const { props } = componentInfo
30
+ | return {
31
+ | componentInfo,
32
+ | renderInfo: hydrate(${wrapped}, componentInfo),
33
+ | }
34
+ |})
35
+ |
36
+ |if (module.hot) {
37
+ | require('@kaliber/build/lib/hot-module-replacement-client')
38
+ | module.hot.accept('./${importPath}', () => {
39
+ | renderInfo = renderInfo.map(({ componentInfo, renderInfo }) => {
40
+ | const { props, endNode } = componentInfo, { container, renderedNodes } = renderInfo
41
+ | return {
42
+ | componentInfo,
43
+ | renderInfo: hydrate(${wrapped}, { nodes: renderedNodes, endNode, container }),
44
+ | }
45
+ | })
46
+ | })
47
+ |}
48
+ |`.split(/^[ \t]*\|/m).join('')
49
+ }
50
+
51
+ function get(o, path) {
52
+ return path.split('.').reduce((result, key) => result && result[key], o )
53
+ }
@@ -0,0 +1,63 @@
1
+ const { relative } = require('path')
2
+
3
+ module.exports = ReactContainerlessUniversalServerLoader
4
+
5
+ function ReactContainerlessUniversalServerLoader(source, map) {
6
+ const filename = relative(this.rootContext, this.resourcePath)
7
+ const importPath = relative(this.context, this.resourcePath)
8
+ const id = filename.replace(/[/.]/g, '_')
9
+ const clientWrapper = get(require('@kaliber/config'), 'kaliber.universal.clientWrapper')
10
+ const serverWrapper = get(require('@kaliber/config'), 'kaliber.universal.serverWrapper')
11
+ return createServerCode({ importPath, id, clientWrapper, serverWrapper })
12
+ }
13
+
14
+ function createServerCode({
15
+ importPath,
16
+ id,
17
+ serverWrapper: serverWrapperPath,
18
+ clientWrapper: clientWrapperPath,
19
+ }) {
20
+ const client = wrap({
21
+ importPath: clientWrapperPath,
22
+ wrapperName: 'ClientWrapper',
23
+ component: '<Component {...props} />',
24
+ })
25
+
26
+ const server = wrap({
27
+ importPath: serverWrapperPath,
28
+ wrapperName: 'ServerWrapper',
29
+ component: '<PropsWrapper serverProps={props} />',
30
+ })
31
+
32
+ return `|import Component from './${importPath}?original'
33
+ |import assignStatics from 'hoist-non-react-statics'
34
+ |import { renderToString } from 'react-dom/server'
35
+ |import { ComponentServerWrapper } from '@kaliber/build/lib/universalComponents'
36
+ |${server.wrapper}
37
+ |${client.wrapper}
38
+ |
39
+ |assignStatics(WrappedForServer, Component)
40
+ |
41
+ |export default function WrappedForServer(props) {
42
+ | return ${server.wrapped}
43
+ |}
44
+ |
45
+ |function PropsWrapper({ serverProps, ...additionalProps }) {
46
+ | const componentName = '${id}'
47
+ | const props = { ...additionalProps, ...serverProps }
48
+ | const renderedComponent = renderToString(${client.wrapped})
49
+ | return <ComponentServerWrapper {...{ componentName, props, renderedComponent }} />
50
+ |}
51
+ |`.replace(/^[\s]*\|/mg, '')
52
+ }
53
+
54
+ function wrap({ wrapperName, component, importPath }) {
55
+ return {
56
+ wrapper: importPath ? `import ${wrapperName} from '${importPath}'` : '',
57
+ wrapped: importPath ? `<${wrapperName} {...props}>${component}</${wrapperName}>` : component,
58
+ }
59
+ }
60
+
61
+ function get(o, path) {
62
+ return path.split('.').reduce((result, key) => result && result[key], o )
63
+ }
@@ -73,12 +73,12 @@ module.exports = function reactUniversalPlugin(webCompilerOptions) {
73
73
  })
74
74
  })
75
75
 
76
- // we claim entries ending with `entry.js` and record them as client entries for the web compiler
76
+ // we claim entries ending with `entry.js` and `.universal.js` and record them as client entries for the web compiler
77
77
  compiler.hooks.claimEntries.tap(p, entries => {
78
78
  const [claimed, unclaimed] = Object.keys(entries).reduce(
79
79
  ([claimed, unclaimed], name) => {
80
80
  const entry = entries[name]
81
- if (entry.endsWith('.entry.js')) claimed[name] = entry
81
+ if (entry.endsWith('.entry.js') || entry.endsWith('.universal.js')) claimed[name] = entry
82
82
  else unclaimed[name] = entry
83
83
 
84
84
  return [claimed, unclaimed]
@@ -97,6 +97,9 @@ module.exports = function reactUniversalPlugin(webCompilerOptions) {
97
97
 
98
98
  When a module marked with `?universal` has been resolved, add the `react-universal-server-loader` to it's
99
99
  loaders and add the module marked with `?universal-client` as client entry.
100
+
101
+ When a module marked with `.universal.js` has been resolved, add the `react-containerless-universal-server-loader` to it's
102
+ loaders and add the module marked with `?continerless-universal-client` as client entry.
100
103
  */
101
104
  compiler.hooks.normalModuleFactory.tap(p, normalModuleFactory => {
102
105
 
@@ -118,6 +121,13 @@ module.exports = function reactUniversalPlugin(webCompilerOptions) {
118
121
  if (!clientEntries[name]) clientEntries[name] = './' + name + '?universal-client'
119
122
  }
120
123
 
124
+ if (path.endsWith('.universal.js') && query !== '?original') {
125
+ loaders.push({ loader: require.resolve('../webpack-loaders/react-containerless-universal-server-loader') })
126
+
127
+ const name = relative(compiler.context, path)
128
+ if (!clientEntries[name]) clientEntries[name] = './' + name + '?containerless-universal-client'
129
+ }
130
+
121
131
  if (path.endsWith('.entry.js')) {
122
132
  data.loaders = [{ loader: require.resolve('../webpack-loaders/ignore-content-loader') }]
123
133
  }
@@ -159,9 +169,13 @@ module.exports = function reactUniversalPlugin(webCompilerOptions) {
159
169
  }
160
170
 
161
171
  function getJavascriptChunkNames(chunk, compiler) {
162
- // find univeral modules in the current chunk (client chunk names) and grab their filenames (uniquely)
172
+ // find universal modules in the current chunk (client chunk names) and grab their filenames (uniquely)
163
173
  return chunk.getModules()
164
- .filter(x => x.resource && (x.resource.endsWith('?universal') || x.resource.endsWith('.entry.js')))
174
+ .filter(x => x.resource && (
175
+ x.resource.endsWith('?universal') ||
176
+ x.resource.endsWith('.entry.js') ||
177
+ x.resource.endsWith('.universal.js')
178
+ ))
165
179
  .map(x => relative(compiler.context, x.resource.replace('?universal', '')))
166
180
  }
167
181
 
@@ -177,6 +191,9 @@ function createWebCompiler(compiler, options) {
177
191
  if (query === '?universal-client')
178
192
  loaders.push({ loader: require.resolve('../webpack-loaders/react-universal-client-loader') })
179
193
 
194
+ if (query === '?containerless-universal-client')
195
+ loaders.push({ loader: require.resolve('../webpack-loaders/react-containerless-universal-client-loader') })
196
+
180
197
  return data
181
198
  })
182
199
  })
@@ -9,7 +9,7 @@ function absolutePathResolverPlugin(path) {
9
9
  const innerRequest = request.request
10
10
  if (innerRequest && innerRequest.startsWith('/')) {
11
11
  const newRequest = Object.assign({}, request, {
12
- path: path,
12
+ path,
13
13
  request: './' + innerRequest.slice(1)
14
14
  })
15
15
  resolver.doResolve(resolver.hooks.resolve, newRequest, 'looking for file in ' + path, resolveContext, callback)