1
1
mirror of https://github.com/primer/css.git synced 2024-12-01 12:42:32 +03:00

Merge branch 'import-primer-module-build' into release-10.6.0

This commit is contained in:
Jon Rohan 2018-06-13 15:14:58 -07:00
commit 62922ac4aa
22 changed files with 448 additions and 57 deletions

View File

@ -1,6 +1,7 @@
{
"lerna": "2.4.0",
"packages": [
"meta/*",
"modules/*",
"tools/*"
],

96
meta/scoreboard/index.js Normal file
View File

@ -0,0 +1,96 @@
const {basename, join, resolve} = require('path')
const PromiseQueue = require('p-queue')
const execa = require('execa')
const globby = require('globby')
const rootDir = resolve(__dirname, '../..')
const lernaConfig = require(join(rootDir, 'lerna.json'))
const modulesDir = join(rootDir, 'modules')
require('console.table')
const unique = list => Array.from(new Set(list)).sort()
const matchAll = (pattern, text) => {
const matches = []
let match
while (match = pattern.exec(text)) {
matches.push(match)
}
return matches
}
const checks = {
'has stories': (module, key) => {
return globby(join(module.path, '**/stories.js'))
.then(files => ({
[key]: files.length > 0 ? 'yes' : 'no'
}))
},
'docs test': (module, key) => {
return execa(join(rootDir, 'script/test-docs'), {
cwd: module.path
})
.then(result => ({[key]: 'pass'}))
.catch(({stderr}) => {
const pattern = /("\.[-\w]+") is not documented/g
const matches = matchAll(pattern, stderr)
.map(match => match[1])
let missing = matches ? Array.from(matches) : []
const max = 5
if (missing.length > max) {
const more = missing.length - max
missing = missing.slice(0, max).concat(`and ${more} more...`)
}
return {
[key]: 'FAIL',
'missing docs': unique(missing).join(', ')
}
})
}
}
const args = process.argv.slice(2)
const modules = args.length
? Promise.resolve(args)
: globby(join(modulesDir, 'primer-*'))
modules
.then(moduleDirs => {
console.log('Found %d module directories', moduleDirs.length)
return moduleDirs
.map(path => ({
path,
name: basename(path),
pkg: require(join(path, 'package.json'))
}))
.filter(({pkg}) => pkg.primer.module_type !== 'meta')
})
.then(modules => {
console.log('Filtered to %d modules (excluding meta-packages)', modules.length)
const queue = new PromiseQueue({concurrency: 3})
for (const module of modules) {
module.checks = {}
for (const [name, check] of Object.entries(checks)) {
queue.add(() => {
// console.warn(`? check: ${module.name} ${name}`)
return check(module, name)
.then(result => {
Object.assign(module.checks, result)
})
})
}
}
console.warn(`Running ${queue.size} checks...`)
return queue.onIdle().then(() => modules)
})
.then(modules => {
console.warn('ran tests on %d modules', modules.length)
const rows = modules.map(({name, checks}) => {
return Object.assign({'package': name}, checks)
})
console.table(rows)
})

View File

@ -0,0 +1,13 @@
{
"private": true,
"name": "primer-scorecard",
"scripts": {
"test": "node index.js"
},
"devDependencies": {
"console.table": "^0.10.0",
"execa": "^0.10.0",
"globby": "^6.1.0",
"p-queue": "^2.4.2"
}
}

View File

@ -72,6 +72,7 @@
border-radius: 0;
}
// FIXME deprecate this
.warning {
padding: $em-spacer-5;
margin-bottom: 0.8em;

View File

@ -10,7 +10,10 @@
"main": "build/index.js",
"primer": {
"category": "product",
"module_type": "components"
"module_type": "components",
"class_whitelist": [
"warning"
]
},
"files": [
"index.scss",

View File

@ -10,7 +10,11 @@
"main": "build/index.js",
"primer": {
"category": "core",
"module_type": "support"
"module_type": "support",
"class_whitelist": [
"octicon",
"rule"
]
},
"files": [
"index.scss",

View File

@ -10,7 +10,11 @@
"main": "build/index.js",
"primer": {
"category": "marketing",
"module_type": "utilities"
"module_type": "utilities",
"class_whitelist": [
"border-??-*",
"position-??-*"
]
},
"files": [
"index.scss",

View File

@ -20,16 +20,21 @@
"test": "npm run test-all-modules && lerna run test",
"test-all-modules": "ava --verbose tests/test-*.js"
},
"dependencies": {
"primer-module-build": "file:tools/primer-module-build",
"stylelint-config-primer": "file:tools/stylelint-config-primer"
},
"devDependencies": {
"@storybook/addon-options": "^3.2.6",
"@storybook/react": "^3.2.12",
"ava": "^0.23.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.6.0",
"babel-preset-minify": "^0.2.0",
"babel-preset-react": "^6.24.1",
"commit-status": "^4.1.0",
"css-loader": "^0.28.4",
"execa": "^0.8.0",
"execa": "^0.10.0",
"fs-extra": "^4.0.2",
"gh-pages": "^1.0.0",
"glob": "^7.1.2",
@ -38,12 +43,13 @@
"isomorphic-fetch": "^2.2.1",
"lerna": "2.4.0",
"lerna-changelog": "^0.7.0",
"node-sass": "^4.5.3",
"minimatch": "^3.0.4",
"node-sass": "^4.9.0",
"npm-run-all": "^4.0.2",
"npx": "^10.2.0",
"octicons": "^6.0.1",
"parse-pairs": "^0.2.2",
"postcss-loader": "^2.0.6",
"primer-module-build": "^1.0.2",
"raw-loader": "^0.5.1",
"react": "^15.6.1",
"react-dom": "^15.6.1",
@ -53,7 +59,6 @@
"semver": "^5.3.0",
"style-loader": "^0.18.2",
"stylelint": "^7.13.0",
"stylelint-config-primer": "^2.0.0",
"unist-util-find-before": "^2.0.1",
"unist-util-parents": "^1.0.0",
"unist-util-select": "^1.5.0",

View File

@ -1,3 +1,3 @@
#!/bin/bash
set -e
$(dirname $0)/npm-run stylelint --quiet --syntax scss **/*.scss
npx stylelint --quiet --syntax scss **/*.scss

View File

@ -1,3 +1,3 @@
#!/bin/bash
set -e
$(dirname $0)/npm-run npm-run-all --serial --silent "$@"
npx npm-run-all --serial --silent "$@"

View File

@ -1,3 +1,3 @@
#!/bin/bash
set -e
$(dirname $0)/npm-run ava --verbose $(dirname $0)/../tests/modules/test-document-styles.js
npx ava --verbose $(dirname $0)/../tests/modules/test-document-styles.js "$@"

View File

@ -1,73 +1,95 @@
const test = require("ava")
const css = require(process.cwd())
const fs = require("fs")
const glob = require("glob")
const {join} = require('path')
const fse = require('fs-extra')
const globby = require('globby')
const test = require('ava')
const minimatch = require('minimatch')
let selectors
let classnames
const cwd = process.cwd()
const css = require(cwd)
const pkg = require(join(cwd, 'package.json'))
const unique = list => Array.from(new Set(list)).sort()
/*
* These are the regular expressions that match what we
* expect to be class name instances in the docs.
* Patterns should group the matched class name(s) such that:
*
* ```js
* const [, klass, ] = pattern.exec(content)
* ```
*/
const classPatterns = [
// HTML class attributes
/class="([^"]+)"/ig,
/:class ?=> "([^"]+)"/g,
/class: "([^"]+)"/g,
// assume that ERB helpers generate an element with the same class
/<%= (\w+)\b/g,
]
const whitelistClasses = (pkg.primer ? pkg.primer.class_whitelist || [] : [])
.concat('js*')
const isWhitelisted = klass => {
return whitelistClasses.some(glob => minimatch(klass, glob))
}
// Find unique selectors from the cssstats selector list
function uniqueSelectors(s) {
s = s.map(s => {
function uniqueSelectors(selectors) {
return unique(selectors.map(s => {
// split multi-selectors into last class used .foo .bar .baz
return s.split(" ").pop()
return s.split(' ').pop()
})
.filter(s => {
// remove any selector that aren't just regular classnames eg. ::hover [type]
// only match exact class selectors
return s.match(/^\.[a-z\-_]+$/ig)
})
// return only the unique selectors
return [...new Set(s)]
}))
}
// From the given glob sources array, read the files and return found classnames
function documentedClassnames(sources) {
const classes = []
const files = sources.reduce((acc, pattern) => {
return acc.concat(glob.sync(pattern))
}, [])
files.forEach(file => {
let match = null
const content = fs.readFileSync(file, "utf8")
classPatterns.forEach(pattern => {
// match each pattern against the source
while (match = pattern.exec(content)) {
// get the matched classnames and split by whitespace into classes
const klasses = match[1].trim().split(/\s+/)
classes.push(...klasses)
}
function getDocumentedClassnames(sources) {
return globby(sources)
.then(paths => {
return Promise.all(paths.map(path => {
return fse.readFile(path, 'utf8')
.then(content => ({path, content}))
}))
})
})
// return only the unique classnames
return Array.from(new Set(classes))
.then(files => {
return files.reduce((classes, {path, content}) => {
classPatterns.forEach(pattern => {
let match
while (match = pattern.exec(content)) {
// get the matched classnames and split by whitespace into classes
let klasses = match[1].trim().split(/\s+/)
classes.push(...klasses)
}
})
return classes
}, [])
})
.then(classes => unique(classes))
}
// Before all the tests get the selectors and classnames
const selectors = uniqueSelectors(css.cssstats.selectors.values)
let classnames
test.before(t => {
selectors = uniqueSelectors(css.cssstats.selectors.values)
classnames = documentedClassnames([
"docs/*.md",
"README.md"
])
return getDocumentedClassnames([
'docs/*.md',
'README.md'
])
.then(_ => (classnames = _))
})
test("Every selector class is documented", t => {
const undocumented = []
selectors.forEach(selector => {
if (!classnames.includes(selector.replace(/^\./, ""))) {
undocumented.push(selector)
selectors.forEach(selector => {
const klass = selector.replace(/^\./, '')
test(`Selector "${selector}" is documented/whitelisted`, t => {
t.plan(1)
if (isWhitelisted(klass)) {
t.pass(`Selector "${selector}" is whitelisted`)
} else {
t.is(classnames.includes(klass), true, `Selector "${selector}" is not documented`)
}
})
t.is(undocumented.length, 0, `I did not find documentation for the "${undocumented.join(", ")}" selector(s) in the ${process.env.npm_package_name} module.`);
})

View File

@ -0,0 +1,14 @@
{
"plugins": [
"eslint-plugin-github"
],
"env": {
"es6": true,
"node": true
},
"extends": [
"plugin:github/recommended",
"plugin:github/es6"
]
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,29 @@
#!/usr/bin/env node
'use strict'
/* eslint-disable no-console */
const meow = require('meow')
const build = require('./')
const cli = meow(`
Usage
$ primer-module-build [file]
File
File. This is required. The file input is the .scss file that
will be built into .css. The build automatically looks in the
node_modules/ directory for any inputs.
Example
$ primer-module-build index.scss
`)
build(cli)
.then(files => {
console.warn('YES! wrote %d files:', files.length)
for (const file of files) console.warn(file)
process.exit(0)
})
.catch(err => {
console.error('NO:', err)
process.exit(1)
})

View File

@ -0,0 +1,19 @@
const build = require('./lib/build')
function InputException(message) {
this.message = message
this.name = "InputException"
}
module.exports = ({input, flags}) => {
if (!input || input.length === 0) {
throw new InputException("You must supply a file to build")
}
const [file] = input
if (!file.match(/\.scss$/)) {
throw new InputException("We are only able to handle .scss files")
}
return build(file, flags)
}

View File

@ -0,0 +1,6 @@
{
"use": ["autoprefixer"],
"autoprefixer": {
"browsers": "> 5%, last 2 firefox versions, last 2 chrome versions, last 2 safari versions, last 2 edge versions, ie 11"
}
}

View File

@ -0,0 +1,69 @@
const {isAbsolute} = require('path')
const cssstats = require('cssstats')
const fse = require('fs-extra')
const nodeSassImport = require('node-sass-import')
const postcss = require('postcss')
const promisify = require('pify')
const sass = require('node-sass')
const sassRender = promisify(sass.render)
function arrayify(value) {
return Array.isArray(value) ? value : [value]
}
module.exports = (src, flags = {}) => {
const cwd = process.cwd()
const sourceFile = isAbsolute(src) ? src : `${cwd}/${src}`
const outputDir = flags.outputDir || `${cwd}/build`
const outputFile = flags.output || `${outputDir}/build.css`
const outputJSFile = flags.outputJS || `${outputDir}/index.js`
const outputDataFile = flags.outputData || `${outputDir}/data.json`
const sassOptions = {
file: sourceFile,
outputStyle: flags.outputStyle || 'compressed',
importer: nodeSassImport,
includePaths: arrayify(flags.include)
}
// console.warn('sass options:', sassOptions)
return fse.mkdirp(outputDir)
.then(() => sassRender(sassOptions))
.then(({css}) => {
function postcssPlugins() {
const postcssrc = (() => {
if (flags.postcssconfig && fse.existsSync(`${cwd}/${flags.postcssconfig}`)) {
return require(`${cwd}/${flags.postcssconfig}`)
} else if (fse.existsSync(`${cwd}/.postcss.json`)) {
return require(`${cwd}/.postcss.json`)
} else {
return require('./.postcss.json')
}
})()
return postcssrc.use.map(name => {
return require(name)(postcssrc[name])
})
}
return postcss(postcssPlugins())
.process(css, {
from: sourceFile,
to: outputFile
})
})
.then(result => {
const data = {
cssstats: cssstats(result.css)
}
const json = JSON.stringify(data)
return Promise.all([
fse.writeFile(outputFile, result.css),
fse.writeFile(outputJSFile, `module.exports = ${json}`),
fse.writeFile(outputDataFile, json)
])
.then(() => [outputFile, outputJSFile, outputDataFile])
})
}

View File

@ -0,0 +1,49 @@
{
"version": "1.0.2",
"name": "primer-module-build",
"description": "Scripts to build primer SCSS modules into CSS and stats",
"homepage": "http://primercss.io/",
"author": "GitHub, Inc.",
"license": "MIT",
"files": [
"index.js",
"cli.js",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/primer/primer-module-build.git"
},
"bugs": {
"url": "https://github.com/primer/primer-module-build/issues"
},
"bin": "cli.js",
"engines": {
"node": ">=4"
},
"scripts": {
"ava": "../../script/npm-run ava --verbose \"tests/**/*.js\"",
"lint": "../../script/npm-run eslint lib/**/*.js *.js tests/**/*.js",
"test": "../../script/npm-run-all lint ava"
},
"keywords": [
"primer",
"build",
"css",
"postcss",
"node-sass"
],
"dependencies": {
"autoprefixer": "^6.7.7",
"cssstats": "^3.2.0",
"meow": "^3.7.0",
"node-sass": "^4.9.0",
"node-sass-import": "^2.0.0",
"pify": "^3.0.0",
"postcss": "^5.2.5"
},
"devDependencies": {
"primer-utilities": "4.9.0",
"tempy": "^0.2.1"
}
}

View File

@ -0,0 +1 @@
@import "primer-utilities/index.scss";

View File

@ -0,0 +1 @@
@import "./primer-package.scss"

View File

@ -0,0 +1,33 @@
const build = require('../lib/build.js')
const test = require('ava')
const tempy = require('tempy')
const {exists} = require('fs-extra')
const {join} = require('path')
function fixture(...path) {
return join(__dirname, 'fixtures', ...path)
}
function assertExists(t, filename) {
return exists(filename)
.then(exists => {
exists ? t.pass() : t.fail(`No such file: ${filename}`)
})
}
test('resolves npm-installed primer package', t => {
return build(fixture('primer-package.scss'), {})
.then(() => t.pass())
})
test('resolves relative paths', t => {
return build(fixture('relative.scss'), {})
.then(() => t.pass())
})
test('writes build.css by default', t => {
const outputDir = tempy.directory()
const outputFile = join(outputDir, 'build.css')
return build(fixture('primer-package.scss'), {outputDir})
.then(() => assertExists(t, outputFile))
})