@magic/css 0.7.40 → 0.7.44

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.
package/README.md CHANGED
@@ -29,8 +29,44 @@ parse/stringify/write css in js
29
29
  npm install --save-exact @magic/css
30
30
  ```
31
31
 
32
+
32
33
  #### usage:
33
34
 
35
+ ##### cli:
36
+
37
+ @magic/css includes a cli script that can handle most usecases the internal javascript api allows.
38
+
39
+ to use this cli from any directory,
40
+ `npm install -g @magic.css` is a useful shortcut.
41
+ after the global install, you can just call `mcss` from anywhere in your terminal.
42
+
43
+ after installation, add:
44
+ `"mcss": "mcss"`
45
+ to your package.json "scripts" section and then
46
+ `npm run mcss`
47
+ to see the help output below.
48
+
49
+ ```
50
+ @magic/css
51
+ commands:
52
+ stringify - convert css in js to css
53
+ parse - convert css in js to an array of key value pairs
54
+ full - get a full result object.
55
+
56
+ flags:
57
+ --minified - output minified css - alias: ["--m", "-m"]
58
+ --help - alias: ["-h"]
59
+ --out - directory to write output files to. omit to print to stdout - alias: ["--o", "-o"]
60
+ --in - directory with source files, needs index.js to exist - alias: ["--i", "-i"]
61
+
62
+ examples:
63
+ mcss parse --in ./styles --out ./css
64
+ mcss stringify --in ./styles --out ./css
65
+ mcss full --in ./styles --out ./css
66
+ ```
67
+
68
+ #### library:
69
+
34
70
  ##### init
35
71
  ```javascript
36
72
  import css from '@magic/css'
@@ -490,6 +526,23 @@ update dependencies
490
526
  #### 0.7.40
491
527
  update dependencies
492
528
 
493
- #### 0.7.41 - unreleased
529
+ #### 0.7.41
530
+ update dependencies
531
+
532
+ #### 0.7.42
533
+ update dependencies
534
+
535
+ #### 0.7.43
536
+ * update dependencies
537
+ * added font v2 to allow handling of local() fonts (see https://magic.github.io/css/#styles-webfonts)
538
+ * woff2 files are placed before woff files
539
+ * use single quotes in output css
540
+ * add cli
541
+
542
+ #### 0.7.44
543
+ * css props can be arrays to provide css overloads `{ color: ['green', 'red'] }` turns into `color: green; color: red;`
544
+ * update dependencies
545
+
546
+ #### 0.7.45 - unreleased
494
547
  ...
495
548
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic/css",
3
- "version": "0.7.40",
3
+ "version": "0.7.44",
4
4
  "author": "Wizards & Witches",
5
5
  "description": "css in js",
6
6
  "license": "AGPL-3.0",
@@ -17,6 +17,10 @@
17
17
  "test": "t --exclude docs example config.js",
18
18
  "calls": "calls"
19
19
  },
20
+ "bin": {
21
+ "mcss": "src/bin/index.js",
22
+ "magic-css": "src/bin/index.js"
23
+ },
20
24
  "engines": {
21
25
  "node": ">=14.15.4"
22
26
  },
@@ -35,11 +39,11 @@
35
39
  "dependencies": {
36
40
  "@magic/cases": "0.0.5",
37
41
  "@magic/deep": "0.1.7",
38
- "@magic/fs": "0.0.17",
42
+ "@magic/fs": "0.0.18",
39
43
  "@magic/log": "0.1.10",
40
44
  "@magic/types": "0.1.16",
41
- "autoprefixer": "10.3.5",
42
- "postcss": "8.3.7"
45
+ "autoprefixer": "10.4.0",
46
+ "postcss": "8.4.5"
43
47
  },
44
48
  "devDependencies": {
45
49
  "@magic-modules/git-badges": "0.0.11",
@@ -47,9 +51,9 @@
47
51
  "@magic-modules/no-spy": "0.0.6",
48
52
  "@magic-modules/pre": "0.0.11",
49
53
  "@magic-themes/docs": "0.0.14",
50
- "@magic/core": "0.0.131",
51
- "@magic/format": "0.0.31",
52
- "@magic/test": "0.1.75"
54
+ "@magic/core": "0.0.133",
55
+ "@magic/format": "0.0.33",
56
+ "@magic/test": "0.1.77"
53
57
  },
54
58
  "keywords": [
55
59
  "css-in-js",
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path'
4
+
5
+ import cli from '@magic/cli'
6
+ import fs from '@magic/fs'
7
+ import log from '@magic/log'
8
+
9
+ import css from '../index.mjs'
10
+
11
+ const cwd = process.cwd()
12
+
13
+ const { args, commands } = cli({
14
+ commands: ['full', 'parse', 'stringify'],
15
+ options: [
16
+ ['--in', '--i', '-i'],
17
+ ['--out', '--o', '-o'],
18
+ ['--minified', '--m', '-m'],
19
+ ],
20
+ single: ['--in', '--out', '--minified'],
21
+ help: {
22
+ name: '@magic/css',
23
+ header: 'css in js pipeline.',
24
+ commands: {
25
+ full: 'get a full result object.',
26
+ parse: 'convert css in js to an array of key value pairs',
27
+ stringify: 'convert css in js to css',
28
+ },
29
+ options: {
30
+ '--in': 'directory with index.js that has a css in js object as default export',
31
+ '--out': 'directory to write output files to. omit to print to stdout',
32
+ '--minified': 'output minified css',
33
+ },
34
+ example: `
35
+ mcss parse --in ./css --out ./css
36
+ mcss stringify --in ./styles --out ./css
37
+ `.trim(),
38
+ },
39
+ })
40
+
41
+ const logResult = style => log.success('mcss output', `\n\n${style}\n`)
42
+
43
+ const maybeWrite = async ({ args, commands, style }) => {
44
+ const outFile = commands.stringify ? 'main.css' : 'theme.js'
45
+
46
+ if (args.out) {
47
+ if (!path.isAbsolute(args.out)) {
48
+ args.out = path.join(cwd, args.out)
49
+ }
50
+
51
+ if (!args.out.endsWith('.css') && !args.out.endsWith('.js')) {
52
+ args.out = path.join(args.out, outFile)
53
+ }
54
+
55
+ const dirname = path.dirname(args.out)
56
+ await fs.mkdirp(dirname)
57
+
58
+ await fs.writeFile(args.out, style)
59
+ } else {
60
+ logResult(style)
61
+ }
62
+
63
+ log.success('mcss is done.')
64
+ }
65
+
66
+ const run = async () => {
67
+ if (!args.in) {
68
+ args.in = cwd
69
+ log.info('--in not specified, using process.cwd()', args.in)
70
+ } else if (!path.isAbsolute(args.in)) {
71
+ args.in = path.join(cwd, args.in)
72
+ }
73
+
74
+ if (!args.in.endsWith('index.js')) {
75
+ args.in = path.join(args.in, 'index.js')
76
+ }
77
+
78
+ const { default: theme } = await import(args.in)
79
+
80
+ const result = await css(theme)
81
+
82
+ if (commands.full) {
83
+ const style = JSON.stringify(result, null, 2)
84
+ maybeWrite({ args, commands, style })
85
+ } else if (commands.stringify) {
86
+ const style = args.hasOwnProperty('minified') ? result.minified : result.css
87
+ maybeWrite({ args, commands, style })
88
+ } else if (commands.parse) {
89
+ const style = JSON.stringify(result.parsed, null, 2)
90
+ maybeWrite({ args, commands, style })
91
+ } else {
92
+ log.error('MISSING_COMMAND', 'Either parse, stringify or full are required.')
93
+ }
94
+ }
95
+
96
+ run()
@@ -29,14 +29,12 @@ const recurseParse = (mod, opts) => {
29
29
  let children = []
30
30
  const props = {}
31
31
 
32
- // media and keyframes are special, they provide a full body of css rules
33
32
  if (parent.startsWith('@keyframes') || parent.startsWith('@media')) {
33
+ // media and keyframes are special, they provide a full body of css rules
34
34
  const i = parse(items)
35
35
  return [parent, i]
36
- }
37
-
38
- // handle font face declarations that are expected to expand
39
- if (parent.startsWith('@font-face')) {
36
+ } else if (parent.startsWith('@font-face')) {
37
+ // handle font face declarations that are expected to expand
40
38
  let i = items
41
39
  if (is.string(items.url)) {
42
40
  i = [items]
@@ -47,14 +45,18 @@ const recurseParse = (mod, opts) => {
47
45
  }
48
46
  }
49
47
 
50
- Object.entries(items).forEach(([name, item]) => {
51
- if (is.object(item)) {
52
- name = getSelector(parent, name)
53
- children = deep.merge(children, recurseParse([name, item], opts))
54
- } else {
55
- props[name] = item
56
- }
57
- })
48
+ if (is.objectNative(items)) {
49
+ Object.entries(items).forEach(([name, item]) => {
50
+ if (is.array(item)) {
51
+ props[name] = item
52
+ } else if (is.objectNative(item)) {
53
+ name = getSelector(parent, name)
54
+ children = deep.merge(children, recurseParse([name, item], opts))
55
+ } else {
56
+ props[name] = item
57
+ }
58
+ })
59
+ }
58
60
 
59
61
  if (is.empty(props)) {
60
62
  if (!is.empty(children)) {
@@ -67,6 +69,11 @@ const recurseParse = (mod, opts) => {
67
69
 
68
70
  const isStyle = style => style && style.length === 2 && is.string(style[0]) && is.object(style[1])
69
71
 
72
+ /*
73
+ * we can not use deep.flatten here, the flattened array has a specific structure:
74
+ * [[key, styles], [key, styles]],
75
+ * deep.flatten would return [key, styles, key, styles]
76
+ */
70
77
  const flat = a => {
71
78
  if (isStyle(a)) {
72
79
  const [k, v] = a
@@ -83,22 +90,28 @@ const flat = a => {
83
90
  }
84
91
  }
85
92
  })
93
+
86
94
  return flattened
87
95
  }
88
96
  }
89
97
 
98
+ /*
99
+ * parse the styles css object into an array of key-value pairs.
100
+ */
90
101
  const parse = (styles, opts = {}) => {
91
102
  // first check if the user sent us a function that resolves to a css object
92
103
  if (is.function(styles)) {
93
104
  styles = styles(opts)
94
105
  }
95
106
 
96
- // this might trigger additionally to the is.function if statement above
97
107
  if (is.array(styles)) {
108
+ // if styles are an array, map over the items
98
109
  styles = styles.map(s => recurseParse(s, opts))
99
- } else if (!is.array(styles) && is.object(styles)) {
110
+ } else if (is.objectNative(styles)) {
111
+ // styles are an object
100
112
  styles = Object.entries(styles).map(s => recurseParse(s, opts))
101
113
  } else {
114
+ // unknown styles
102
115
  log.error('invalid styles received', styles)
103
116
  }
104
117
 
@@ -0,0 +1,150 @@
1
+ import is from '@magic/types'
2
+ import deep from '@magic/deep'
3
+ import log from '@magic/log'
4
+
5
+ import { recurseStringify } from './recurseStringify.mjs'
6
+
7
+ export const fontFileTypes = {
8
+ eot: url => `src: url('${url}.eot'); src: url('${url}.eot#iefix') format('embedded-opentype')`,
9
+ woff2: url => `url('${url}.woff2') format('woff2')`,
10
+ woff: url => `url('${url}.woff') format('woff')`,
11
+ ttf: url => `url('${url}.ttf') format('truetype')`,
12
+ svg: (url, family) => `url('${url}.svg#${family}') format('svg')`,
13
+ }
14
+
15
+ const fontV1 = (name, font) => {
16
+ log.warn('W_DEPRECATED', 'font name', font.family, 'is declared in a deprecated way.')
17
+ log('See: https://magic.github.io/css/#styles-webfonts for more information.')
18
+
19
+ let { family, url, types = 'woff2', weights = 400, styles = 'normal', ...rest } = font
20
+
21
+ if (!url.endsWith('/')) {
22
+ url += '/'
23
+ }
24
+
25
+ if (!is.array(weights)) {
26
+ weights = [weights]
27
+ }
28
+
29
+ if (!is.array(types)) {
30
+ types = [types]
31
+ }
32
+
33
+ if (!is.array(styles)) {
34
+ styles = [styles]
35
+ }
36
+
37
+ return weights.map(fontWeight => {
38
+ return styles.map(fontStyle => {
39
+ const weightStyleUrl = `${url}${family}-${fontWeight}-${fontStyle}`
40
+
41
+ let fontString = `${name} ${recurseStringify({
42
+ fontFamily: `'${family}'`,
43
+ fontStyle,
44
+ fontWeight,
45
+ ...rest,
46
+ })}`
47
+
48
+ let fontFileString = 'src:'
49
+ if (types.includes('eot')) {
50
+ fontFileString = ''
51
+ }
52
+
53
+ const fontFileStrings = []
54
+ Object.entries(fontFileTypes).map(([name, fn]) => {
55
+ if (types.includes(name)) {
56
+ const str = fn(weightStyleUrl, family)
57
+ fontFileStrings.push(str)
58
+ }
59
+ })
60
+
61
+ fontFileString += fontFileStrings.join(',') + ';'
62
+
63
+ fontString = fontString.replace('}\n', `${fontFileString} }\n`)
64
+ return fontString
65
+ })
66
+ })
67
+ }
68
+
69
+ const fontV2 = (name, font) => {
70
+ let { family, url, types = 'woff2', styles, ...rest } = font
71
+
72
+ if (!url.endsWith('/')) {
73
+ url += '/'
74
+ }
75
+
76
+ if (!is.array(types)) {
77
+ types = [types]
78
+ }
79
+
80
+ const fontStrings = Object.entries(styles).map(entry => {
81
+ const [fontStyle, weights] = entry
82
+
83
+ return Object.entries(weights).map(([fontWeight, local]) => {
84
+ const weightStyleUrl = `${url}${family}-${fontWeight}-${fontStyle}`
85
+
86
+ const fontFileStrings = []
87
+
88
+ let fontFileString = ''
89
+ if (types.includes('eot')) {
90
+ fontFileString = ''
91
+ }
92
+
93
+ local.forEach(l => {
94
+ fontFileStrings.push(`local('${l}')`)
95
+ })
96
+
97
+ Object.entries(fontFileTypes).map(([name, fn]) => {
98
+ if (types.includes(name)) {
99
+ const str = fn(weightStyleUrl, family)
100
+ fontFileStrings.push(str)
101
+ }
102
+ })
103
+
104
+ fontFileString += fontFileStrings.join(',')
105
+
106
+ const props = recurseStringify({
107
+ fontFamily: `'${family}'`,
108
+ fontStyle,
109
+ fontWeight,
110
+ src: fontFileString,
111
+ ...rest,
112
+ })
113
+
114
+ const fontString = `${name} ${props}`
115
+
116
+ return fontString
117
+ })
118
+ })
119
+
120
+ return deep.flatten(fontStrings).join('\n')
121
+ }
122
+
123
+ export const fontFaces = ({ res, name, items }) => {
124
+ if (is.array(items)) {
125
+ const fontStrings = items.map(font => {
126
+ if (is.objectNative(font.styles)) {
127
+ return fontV2(name, font)
128
+ } else {
129
+ return fontV1(name, font)
130
+ }
131
+ })
132
+
133
+ return deep.flatten(fontStrings).join('\n')
134
+ } else {
135
+ const { fontFamily, fontDir = '', fontRoot, ...rest } = items
136
+ res = `${name} ${recurseStringify({ fontFamily: `'${fontFamily}'`, ...rest })}`
137
+
138
+ const eotString = `src: url('${fontDir}${fontFamily}.eot');`
139
+
140
+ const srcString = `${eotString} src: ${[
141
+ `url('${fontDir}${fontFamily}.eot#iefix') format('embedded-opentype')`,
142
+ `url('${fontDir}${fontFamily}.ttf') format('truetype')`,
143
+ `url('${fontDir}${fontFamily}.woff2') format('woff2')`,
144
+ `url('${fontDir}${fontFamily}.woff') format('woff')`,
145
+ `url('${fontDir}${fontFamily}.svg#${fontFamily}') format('svg');`,
146
+ ].join(', ')}`
147
+
148
+ return res.replace('}\n', `${srcString} }\n`)
149
+ }
150
+ }
@@ -4,11 +4,34 @@ import autoprefixer from 'autoprefixer'
4
4
  import parse from '../parse/index.mjs'
5
5
  import recurseStringify from './recurseStringify.mjs'
6
6
 
7
+ import { fontFaces } from './fontFaces.mjs'
8
+
9
+ const keyframes = ({ name, items, plugins }) => `${name} { ${recurseStringify(items, plugins)} }\n`
10
+ const mediaqueries = ({ name, items, plugins }) =>
11
+ `${name} { ${recurseStringify(items, plugins)} }\n`
12
+
13
+ /*
14
+ * stringify the styles css object into a valid css string.
15
+ */
7
16
  const stringify = async (styles, opts = {}) => {
17
+ const { plugins = {} } = opts
18
+
19
+ plugins.stringify = {
20
+ '@font-face': fontFaces,
21
+ '@keyframes': keyframes,
22
+ '@media': mediaqueries,
23
+ ...plugins.stringify,
24
+ }
25
+
8
26
  const parsed = parse(styles, opts)
9
27
 
10
- const stringified = recurseStringify(parsed)
28
+ const stringified = recurseStringify(parsed, plugins.stringify, opts)
29
+
30
+ /*
31
+ * test the resulting css using postcss and autoprefix vendors where needed.
32
+ */
11
33
  const result = await postcss([autoprefixer]).process(stringified, { from: undefined })
34
+
12
35
  result.warnings().forEach(warn => {
13
36
  console.warn(warn.toString())
14
37
  })
@@ -1,8 +1,18 @@
1
1
  import cases from '@magic/cases'
2
+ import is from '@magic/types'
2
3
 
3
4
  export const stringifyProps = props =>
4
5
  Object.entries(props)
5
- .map(([k, v]) => `${cases.kebab(k)}: ${v};`)
6
+ .map(([k, v]) => {
7
+ const kebab = cases.kebab(k)
8
+
9
+ // handle css overloads (color: ['green', 'red'] turns into "color: green; color: red;")
10
+ if (is.array(v)) {
11
+ return v.map(o => `${kebab}: ${o};`).join(' ')
12
+ }
13
+
14
+ return `${kebab}: ${v};`
15
+ })
6
16
  .join(' ')
7
17
 
8
18
  export default stringifyProps
@@ -2,102 +2,27 @@ import is from '@magic/types'
2
2
 
3
3
  import stringifyProps from './props.mjs'
4
4
 
5
- export const fontFaces = ({ res, name, items }) => {
6
- if (is.array(items)) {
7
- const fontStrings = []
5
+ export const recurseStringify = (res, plugins = [], opts = {}) => {
6
+ if (is.array(res)) {
7
+ const [name, items] = res
8
8
 
9
- items.forEach(font => {
10
- let { family, url, types = 'woff2', weights = 400, styles = 'normal', ...rest } = font
11
-
12
- if (!url.endsWith('/')) {
13
- url += '/'
14
- }
15
- if (!is.array(weights)) {
16
- weights = [weights]
17
- }
18
- if (!is.array(styles)) {
19
- styles = [styles]
20
- }
21
- if (!is.array(types)) {
22
- types = [types]
23
- }
24
-
25
- weights.forEach(fontWeight => {
26
- styles.forEach(fontStyle => {
27
- const weightStyleUrl = `${url}${family}-${fontWeight}-${fontStyle}`
28
-
29
- let fontString = `${name} ${recurseStringify({
30
- fontFamily: `"${family}"`,
31
- fontStyle,
32
- fontWeight,
33
- ...rest,
34
- })}`
35
-
36
- const fontFileTypes = {
37
- eot: url =>
38
- `src: url('${url}.eot'); src: url('${url}.eot#iefix') format('embedded-opentype')`,
39
- ttf: url => `url('${url}.ttf') format('truetype')`,
40
- woff: url => `url('${url}.woff') format('woff')`,
41
- woff2: url => `url('${url}.woff2') format('woff2')`,
42
- svg: url => `url('${url}.svg#${family}') format('svg')`,
43
- }
44
-
45
- let fontFileString = 'src:'
46
- if (types.includes('eot')) {
47
- fontFileString = ''
48
- }
49
-
50
- const fontFileStrings = []
51
- Object.entries(fontFileTypes).map(([name, fn]) => {
52
- if (types.includes(name)) {
53
- const str = fn(weightStyleUrl)
54
- fontFileStrings.push(str)
55
- }
56
- })
57
-
58
- fontFileString += fontFileStrings.join(',') + ';'
59
-
60
- fontString = fontString.replace('}\n', `${fontFileString} }\n`)
61
- fontStrings.push(fontString)
62
- })
9
+ if (is.string(name)) {
10
+ let result = ''
11
+ Object.entries(plugins).forEach(([lookup, fn]) => {
12
+ if (name.startsWith(lookup)) {
13
+ result = fn({ name, items, plugins })
14
+ }
63
15
  })
64
- })
65
-
66
- return fontStrings.join('\n')
67
- } else {
68
- const { fontFamily, fontDir = '', fontRoot, ...rest } = items
69
- res = `${name} ${recurseStringify({ fontFamily: `"${fontFamily}"`, ...rest })}`
70
-
71
- const eotString = `src: url('${fontDir}${fontFamily}.eot');`
72
-
73
- const srcString = `${eotString} src: ${[
74
- `url('${fontDir}${fontFamily}.eot#iefix') format('embedded-opentype')`,
75
- `url('${fontDir}${fontFamily}.ttf') format('truetype')`,
76
- `url('${fontDir}${fontFamily}.woff') format('woff')`,
77
- `url('${fontDir}${fontFamily}.woff2') format('woff2')`,
78
- `url('${fontDir}${fontFamily}.svg#${fontFamily}') format('svg');`,
79
- ].join(', ')}`
80
16
 
81
- return res.replace('}\n', `${srcString} }\n`)
82
- }
83
- }
84
-
85
- export const recurseStringify = mod => {
86
- let res = mod
87
-
88
- if (is.array(mod)) {
89
- const [name, items] = mod
90
- if (is.string(name)) {
91
- if (name.startsWith('@keyframes') || name.startsWith('@media')) {
92
- return `${name} { ${recurseStringify(items)} }\n`
93
- } else if (name.startsWith('@font-face')) {
94
- return fontFaces({ res, name, items })
17
+ if (result) {
18
+ return result
95
19
  }
96
20
  }
97
21
 
98
- return mod.map(recurseStringify).join(' ')
99
- } else if (is.object(mod)) {
100
- return `{ ${stringifyProps(mod)} }\n`
22
+ return res.map(r => recurseStringify(r, plugins, opts)).join(' ')
23
+ } else if (is.object(res)) {
24
+ const stringified = stringifyProps(res)
25
+ return `{ ${stringified} }\n`
101
26
  }
102
27
 
103
28
  return res