Single source of truth distributes shared config between layers, allowing extension #17
Changes from 105 commits
1341073
6829588
3c57c7f
918c53b
b1c038a
ea14b36
64e9f7a
4d78ca4
9fd4da1
4da28c9
704a17f
ce9ffcf
f69eed0
b7300f2
468ee6e
168ae14
595f7c0
dd8ca1e
cdde977
ddd2928
92f7a68
8d2642d
ecce35c
817c64a
a348557
5c1e667
1a41085
9fabaa9
4b67b5e
8540b54
e8f7782
65b8c7b
9b18082
dadc387
cf6dc69
f1033c7
f0b0809
e3cb7f2
576c4ce
b2417d4
b9fa354
38fcfa1
829d17f
328f837
b6fe2c2
4f0afb6
a996752
bd4be20
ca15ea4
a0e6b3d
7cafe05
d25a934
4d22ec8
c91f9c9
ba2e2a5
7f1312d
d995c17
5159f90
80d5839
e4e0124
36fec82
40d7ddd
dd892e7
3516df4
976e61a
67750fa
8301116
87b0bc3
245f842
8eafdf0
c827011
3ec46d1
2a655be
48e2f4c
140f4eb
430021b
e2b93ac
bbc8010
54842b8
bd7ec6c
a5c3ce5
36ea903
2974d2c
5ad6ac4
5dabe74
d5808ed
8e6d0bf
333f94a
b9e18eb
1abf3ea
d9b597e
fb8f8ca
edd6110
2bff773
96dce20
4478d0e
886a026
cdfb87a
6f49424
72a7787
5cc541c
faf32b8
9304432
0fd67da
a3181df
84668ab
8e07413
122989f
dc169c7
9c71d31
119c9c5
3f6f103
df13d5d
9a41b66
6a0fbc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
FROM node:8.12.0-slim | ||
FROM node:10.12.0-slim | ||
WORKDIR /patterns-core | ||
|
||
COPY package.json package-lock.json ./ | ||
RUN npm install | ||
|
||
COPY . ./ | ||
RUN node_modules/.bin/gulp build | ||
RUN node_modules/.bin/gulp assemble |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,118 @@ | ||
Libero pattern library | ||
====================== | ||
|
||
## Pipeline | ||
Libero pattern library | ||
====================== | ||
|
||
## Developing patterns with the pattern library | ||
[No patterns yet! | ||
|
||
The build process uses a Node.js container image to build all assets, and copy them out of the container into `export/`. | ||
This section is a work in progress.] | ||
|
||
|
||
### Configuration | ||
N.B. When configuration files are changed, the config needs to be regenerated, either by running `node ./libero-config/bin/distributeConfig.js` or the Gulp task `distributeConfig`. | ||
|
||
#### Uses of configuration | ||
Configuration is used for two things: | ||
|
||
1. to be the single source of truth for knowledge that needs to be shared across across front end technology boundaries. For example, media query breakpoint values need to exist in the styling layer, but they are also often needed by JavaScript. Note that for flexibility all configuration could be maintained using this system in order to make it easier to later distribute configuration if it suddenly becomes necessary. | ||
|
||
1. whilst enabling the single source of truth, configuration must also be able to be changed as required in a manageable way. The breakpoints, colors, baseline grid measures etc may not be the same between implementations, whilst enabling the ability to easily reuse most of the config and only tweak the odd value if a light touch is needed. | ||
|
||
#### Anatomy of configuration | ||
(All config file code examples are taken from `/libero-config/config--libero-default.js`.) | ||
|
||
##### Simple example | ||
`config.data` is where you define your configuration data. | ||
Here `config.data` defines the the `small` and `medium` site breakpoints: | ||
|
||
``` | ||
config.data.breakpoint = {site: {}}; | ||
config.data.breakpoint.site.small = 480; | ||
config.data.breakpoint.site.medium = 730; | ||
``` | ||
|
||
`config.layerAllocations` specifies which technology layers the properties of `config.data` are distributed to. Continuing the above example: | ||
``` | ||
config.layerAllocations = { | ||
sass: ['breakpoint'], | ||
js: ['breakpoint'], | ||
template: ['breakpoint'] }; | ||
``` | ||
specifies that the `breakpoint` config must be distributed to all three available layers: the sass, JavaScript and the templating layer. | ||
|
||
##### Advanced example | ||
Sometimes configuration values depend on other configuration values, for example measures in a grid system. To be able to maintain these relationships even when the underlying predicate value may be modified by a later-loading config file, the calculation of the final value determined by these relationships must be deferred until all specified configurations are loaded and parsed. This is achieved by specifying these simple mathematical expressions in the format: | ||
``` | ||
'!expression [some simple mathematical expression]' | ||
``` | ||
Using this we can specify the baseline grid as: | ||
``` | ||
config.data.baselinegrid = {space: {}}; | ||
config.data.baselinegrid.space.extra_small_in_px = 12; | ||
config.data.baselinegrid.space.small_in_px = '!expression baselinegrid.space.extra_small_in_px * 2'; | ||
config.data.baselinegrid.space.smallish_in_px = '!expression baselinegrid.space.small_in_px * 1.5'; | ||
config.data.baselinegrid.space.medium_in_px = '!expression baselinegrid.space.small_in_px * 2'; | ||
... | ||
``` | ||
The result is that `config.data.baselinegrid.space.small_in_px` will have the value twice that of whatever the final value of `config.data.baselinegrid.space.extra_small_in_px`is, *even if `config.data.baselinegrid.space.extra_small_in_px` is modified by a later loading config*. This provides a way of reusing the essentials of the baseline grid system, but basing it on a different key value as required. | ||
|
||
#### Distributing configuration | ||
##### Distributing to SASS | ||
Each property of `config.data` specified in `config.layerAllocations.sass` is eventually written as a SASS file to `/source/css/sass/derived-from-config/_variables--[propertyname].sass`. Each of these files contains the SASS variables describing the config for that property. Looking at the `breakpoint` example again, this config | ||
|
||
``` | ||
// specified in a config file | ||
config.data.breakpoint = {site: {}}; | ||
config.data.breakpoint.site.small = 480; | ||
config.data.breakpoint.site.medium = 730; | ||
|
||
config.layerAllocations.sass = ['breakpoint']; | ||
``` | ||
|
||
generates this file: | ||
``` | ||
// /source/css/sass/derived-from-config/_variables--breakpoint.sass | ||
$breakpoint-site-small: 480; | ||
$breakpoint-site-medium: 730; | ||
``` | ||
##### Distributing to JavaScript | ||
Each property of `config.data` specified in `config.layerAllocations.jss` is eventually written to `/source/js/derived-from-config/configForJs.json`. Looking at the `breakpoint` example again, this config: | ||
|
||
```js | ||
// specified in a config file | ||
config.data.breakpoint = {site: {}}; | ||
config.data.breakpoint.site.small = 480; | ||
config.data.breakpoint.site.medium = 730; | ||
config.layerAllocations.js = ['breakpoint']; | ||
``` | ||
|
||
adds this into `configForJs.json`: | ||
``` | ||
// /source/js/derived-from-config/configForJs.json | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty wordy, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. I like |
||
... | ||
{"breakpoint":{"site":{"small":480,"medium":730}}} | ||
... | ||
``` | ||
##### Distributing to templates | ||
[Not yet implemented] | ||
|
||
#### Modifying configuration | ||
##### Default configuration | ||
Default configuration is supplied by `/libero-config/config--libero-default.js`. If this is sufficient, the rest of this section may be safely ignored. | ||
|
||
##### Changing configuration | ||
Do not change the contents of the default config file `/libero-config/config--libero-default.js` directly. | ||
|
||
Any changes to the configuration should be effected by placing one or more custom configuration files into `/libero-config/`, and registering the file name(s) in `/libero-config/configRegister.json`. Any files listed here are loaded as config files. The order of the files listed defines their load order. This is important when namespace clashes occur: when this happens the clashing name that was loaded last wins. This is how specific configuration properties are overridden. | ||
|
||
##### Swapping out configuration wholesale | ||
Supply your own config file(s), add appropriate references to `/libero-config/configRegister.js`, and remove mention of `configs--libero-default.js` from `/libero-config/configRegister.js`. | ||
|
||
##### Keep default configuration but augment or override some of its properties | ||
Supply your own config file(s), add appropriate references to `/libero-config/configRegister.js`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably don't actually want to support this? We know that we want to support extensions of the pattern library, but forking isn't really the way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something to think about. Not sure just at the moment how else we might handle extensions. Will ponder. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will remove documentation, but leave implementation in for now, undocumented & so subject to change. |
||
|
||
## Pipeline | ||
|
||
The build process uses a Node.js container image to build all assets, and copy them out of the container into `export/`. | ||
|
||
`export/` can then be packaged to be released on Github, or reused elsewhere. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
'use strict'; | ||
|
||
const del = require('del'); | ||
const {exec} = require('child_process'); | ||
const flatten = require('gulp-flatten'); | ||
const gulp = require('gulp'); | ||
const mergeStreams = require('merge-stream'); | ||
|
@@ -46,6 +47,7 @@ function buildConfig(invocationArgs, sourceRoot, testRoot, exportRoot) { | |
config.dir.src.images = `${config.sourceRoot}images/`; | ||
config.dir.src.fonts = `${config.sourceRoot}fonts/`; | ||
config.dir.src.templates = `${config.sourceRoot}_patterns/`; | ||
config.dir.src.js = `${config.sourceRoot}js/`; | ||
|
||
config.dir.test.sass = `${config.testRoot}sass/`; | ||
|
||
|
@@ -70,6 +72,10 @@ function buildConfig(invocationArgs, sourceRoot, testRoot, exportRoot) { | |
config.files.src.images = [`${config.dir.src.images}/*`, `${config.dir.src.images}/**/*`]; | ||
config.files.src.fonts = [`${config.dir.src.fonts}/*`, `${config.dir.src.fonts}/**/*`]; | ||
config.files.src.templates = [`${config.dir.src.templates}/*.twig`, `${config.dir.src.templates}/**/*.twig`]; | ||
config.files.src.derivedConfigs = [ | ||
`${config.dir.src.sass}derived-from-config/**/*`, | ||
`${config.dir.src.js}derived-from-config/**/*` | ||
]; | ||
|
||
config.files.test.sass = `${config.dir.test.sass}**/*.spec.scss`; | ||
config.files.test.sassTestsEntryPoint = `${config.dir.test.sass}test_sass.js`; | ||
|
@@ -151,14 +157,36 @@ gulp.task('exportPatterns', ['patternsExport:clean'], () => { | |
|
||
}); | ||
|
||
gulp.task('sharedConfig:clean', () => { | ||
return del(config.files.src.derivedConfigs); | ||
}); | ||
|
||
gulp.task('distributeSharedConfig', ['sharedConfig:clean'], (done) => { | ||
exec('node ./libero-config/bin/distributeConfig.js', (err, stdout, stderr) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can a sub-process be avoided? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly. I originally tried to integrate it into the gulp process more effectively, but it didn't work. I think it's time for another go though. |
||
console.log(stdout); | ||
console.log(stderr); | ||
done(err); | ||
}); | ||
}); | ||
|
||
gulp.task('sass:watch', () => { | ||
return gulp.watch([config.files.src.sass, config.files.test.sass], ['css:generate']); | ||
}); | ||
|
||
gulp.task('default', done => { | ||
gulp.task('assemble', done => { | ||
runSequence( | ||
'distributeSharedConfig', | ||
'build', | ||
done, | ||
); | ||
}); | ||
|
||
gulp.task('default', done => { | ||
runSequence( | ||
'assemble', | ||
'exportPatterns', | ||
done, | ||
); | ||
}); | ||
|
||
gulp.task('watch', ['sass:watch']); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
const Color = require('color'); | ||
const deepIterator = require('deep-iterator').default; | ||
const flatten = require('flat'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const {promisify} = require('util'); | ||
|
||
const writeFileAsync = promisify(fs.writeFile); | ||
|
||
/** | ||
* Distributes specified config to appropriate layers (sass, js, templates) | ||
* @type {module.ConfigDistributor} | ||
*/ | ||
module.exports = class ConfigDistributor { | ||
|
||
constructor() { | ||
this.paths = { | ||
out: { | ||
sassVariablesFileNameRoot: '/source/css/sass/derived-from-config/_variables--', | ||
jsonFileName: '/source/js/derived-from-config/configForJs.json' | ||
} | ||
}; | ||
} | ||
|
||
distribute(configPaths, configGenerator) { | ||
|
||
console.log('Distributing config...'); | ||
|
||
return configGenerator.generateConfig(configPaths) | ||
|
||
.then((config) => { | ||
return Promise.all( | ||
[ | ||
this.distributeToSass(config.layerAllocations.sass, config.data), | ||
this.distributeToJs(config.layerAllocations.js, config.data), | ||
] | ||
) | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indentation's a bit hard to read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I'll fix that. |
||
|
||
.catch(err => { | ||
console.error(err.message); | ||
process.exit(1); | ||
}); | ||
} | ||
|
||
distributeToJs(allocations, data) { | ||
return ConfigDistributor.writeFile( | ||
ConfigDistributor.processForJs(allocations, data), | ||
this.paths.out.jsonFileName | ||
); | ||
} | ||
|
||
distributeToSass(allocations, data) { | ||
|
||
const fileWritePromises = []; | ||
|
||
// Each allocation is written to a separate file | ||
allocations.forEach((allocation) => { | ||
const dataForAllocation = {}; | ||
dataForAllocation[allocation] = data[allocation]; | ||
const processedItemData = ConfigDistributor.processForSass(dataForAllocation); | ||
const outFileName = | ||
`${this.paths.out.sassVariablesFileNameRoot}${allocation}.scss`; | ||
fileWritePromises.push( | ||
new Promise((resolve) => { | ||
resolve(ConfigDistributor.writeFile(processedItemData, outFileName)); | ||
}) | ||
); | ||
}); | ||
|
||
return Promise.all(fileWritePromises).catch(err => { throw err; } ); | ||
|
||
} | ||
|
||
static processForJs(allocations, data) { | ||
const processed = {}; | ||
allocations.forEach((allocation) => { | ||
processed[allocation] = data[allocation]; | ||
}); | ||
return JSON.stringify(processed); | ||
} | ||
|
||
static processForSass(data) { | ||
const deepData = deepIterator(data); | ||
for (let {parent, key, value} of deepData) { | ||
if (value instanceof Color) { | ||
parent[key] = value.rgb().string(); | ||
} | ||
} | ||
|
||
return Object.entries(flatten(data, {delimiter: '-'})) | ||
.reduce((carry, pair) => { | ||
const [key, value] = pair; | ||
return `${carry}$${key}: ${value};\n`; | ||
}, ''); | ||
} | ||
|
||
static writeDirectory(path) { | ||
return new Promise((resolve, reject) => { | ||
fs.mkdir(path, { recursive: true}, (err) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
|
||
static async writeFile(data, outPath) { | ||
let projectRootPath = process.cwd(); | ||
const matched = projectRootPath.match(/^.*\/libero-config\/bin.*$/); | ||
if (matched) { | ||
projectRootPath = path.resolve(path.join(process.cwd(), '../..')); | ||
} | ||
const outPathDirectoryComponent = outPath.substring(0, outPath.lastIndexOf('/') + 1); | ||
const fullDirectoryPath = path.join(projectRootPath, outPathDirectoryComponent); | ||
await this.writeDirectory(fullDirectoryPath); | ||
|
||
const filenameComponent = outPath.substring(outPath.lastIndexOf('/') + 1); | ||
return writeFileAsync(path.join(fullDirectoryPath, filenameComponent), data) | ||
.then(() => { | ||
console.log(`Written config to ${path.join(outPathDirectoryComponent, filenameComponent)}`); | ||
}) | ||
.catch(err => { throw err }); | ||
} | ||
|
||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be generating all the 'variables' files? If so,
/source/css/sass/_variables--[propertyname].sass
or/source/css/sass/variables/[propertyname].sass
?(Also,
sass
insidecss
?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I though that it'd only need to generate those that are either extended by additional config, or that contain knowledge shared between technology layers, but now I'm thinking that it should generate all of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sass
insidecss
because of how it's used during the export process. I'm not wedded to that structure, but a change is outside the scope of this PR.