├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── node_modules └── cool-styles │ ├── package.json │ └── styles.css ├── package.json ├── plugin.js └── tests ├── cases ├── 01-simple │ ├── expected.js │ ├── index.js │ └── styles.css ├── 02-compose │ ├── expected.js │ ├── index.js │ ├── styles.css │ └── things.css ├── 03-values │ ├── expected.js │ ├── index.js │ ├── styles.css │ └── things.css ├── 04-compose-from-npm │ ├── expected.js │ ├── index.js │ └── styles.css └── 05-compose-from-package │ ├── expected.js │ ├── index.js │ └── styles.css └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Josh Johnston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cmify (aka node-css-modules) 2 | ==== 3 | 4 | [![Build Status](https://api.travis-ci.org/joshwnj/cmify.svg)](https://travis-ci.org/joshwnj/cmify) 5 | 6 | A node-first approach to [CSS Modules](https://github.com/css-modules/css-modules), so you can use CSS Modules on the server without any extra tools. 7 | 8 | Example 9 | ---- 10 | 11 | ```js 12 | var cmify = require('cmify') 13 | var styles = cmify.load('./styles.css') 14 | 15 | console.log('Generated classnames:', styles) 16 | console.log('Generated CSS:', cmify.getAllCss()) 17 | ``` 18 | 19 | For a complete example take a look at [cmify-example](https://github.com/joshwnj/cmify-example) 20 | 21 | Building for the browser 22 | ---- 23 | 24 | With browserify: 25 | 26 | ``` 27 | npm install -D browserify 28 | 29 | browserify -p cmify/plugin src/index.js 30 | ``` 31 | 32 | With hot module reloading: 33 | 34 | ``` 35 | npm install -D watchify browserify-hmr 36 | 37 | watchify -p browserify-hmr -p cmify/plugin src/index.js -o dist/index.js 38 | ``` 39 | 40 | Saving the generated CSS to a file: 41 | 42 | ``` 43 | npm install -D browserify 44 | 45 | browserify -p [cmify/plugin -o lib/out.css] src/index.js 46 | ``` 47 | 48 | _Note: `-o` is an alias for `--outfile`._ 49 | 50 | How to add postcss plugins 51 | ---- 52 | 53 | You can add postcss plugins before or after the core CSS Modules transformation: 54 | 55 | ``` 56 | const cmify = require('cmify') 57 | 58 | cmify.init({ 59 | cssBefore: [ /* array of postcss plugins */ ], 60 | cssAfter: [ /* array of postcss plugins */ ], 61 | generateScopedName: (originalFn) => function (exportedName, filename) => String 62 | }) 63 | ``` 64 | 65 | `cmify.init` is optional. It should only be called once, and needs to be called before the first `cmify.load`. 66 | 67 | Thanks 68 | ---- 69 | 70 | to the [CSS Modules team](https://github.com/orgs/css-modules/people) and [contributors](https://github.com/joshwnj/cmify/graphs/contributors) 71 | 72 | License 73 | ---- 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const resolve = require('resolve') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const postcss = require('postcss') 7 | const Parser = require('postcss-modules-parser') 8 | const DepGraph = require('dependency-graph').DepGraph 9 | const caller = require('caller') 10 | 11 | const Values = require('postcss-modules-values') 12 | const ExtractImports = require('postcss-modules-extract-imports') 13 | const LocalByDefault = require('postcss-modules-local-by-default') 14 | const Scope = require('postcss-modules-scope') 15 | 16 | const defaultPlugins = [ 17 | Values, 18 | LocalByDefault, 19 | ExtractImports, 20 | Scope 21 | ] 22 | 23 | let plugins = [] 24 | 25 | let _baseDir 26 | let _resultCache 27 | let _depGraph 28 | 29 | 30 | function generateScopedName() { 31 | // only use the relevant part of the filename 32 | Scope.generateScopedName = (function () { 33 | const orig = Scope.generateScopedName 34 | return function (exportedName, filename) { 35 | const relFilename = path.relative(_baseDir, filename) 36 | return orig(exportedName, relFilename) 37 | } 38 | })() 39 | } 40 | 41 | function parseCss (css, filename, visited) { 42 | const parentId = filename 43 | const cachedResult = getResultById(filename) 44 | if (cachedResult) { 45 | return cachedResult 46 | } 47 | 48 | const parser = new Parser({ fetch: fetch }) 49 | const instance = postcss(plugins.concat(parser)) 50 | 51 | function fetch (_to, from) { 52 | const to = _to.replace(/^["']|["']$/g, '') 53 | const filename = /\w/i.test(to[0]) 54 | ? resolve.sync(to, {packageFilter: mapStyleEntry}) 55 | : path.resolve(path.dirname(from), to) 56 | 57 | const css = fs.readFileSync(filename, 'utf8') 58 | return subCmify(css, filename, parentId, visited) 59 | } 60 | 61 | const lazyResult = instance.process(css, { from: filename }) 62 | lazyResult.warnings().forEach(function (w) { console.warn(w.text) }) 63 | 64 | const tokens = lazyResult.root.tokens 65 | 66 | return { 67 | tokens: tokens, 68 | css: lazyResult.root.toString() 69 | } 70 | } 71 | 72 | function mapStyleEntry (pkg) { 73 | if (!pkg.main && pkg.style) { pkg.main = pkg.style } 74 | return pkg 75 | } 76 | 77 | function getResultById (id) { 78 | return _resultCache[id] 79 | } 80 | 81 | function storeResultById (id, result) { 82 | if (_resultCache[id]) { 83 | console.warn('Overwriting cache for ', id, result) 84 | } 85 | _resultCache[id] = result 86 | } 87 | 88 | cmify.invalidateById = function invalidateById (id) { 89 | delete _resultCache[id] 90 | } 91 | 92 | cmify.getDependencies = function getDependencies () { 93 | return _depGraph.overallOrder() 94 | } 95 | 96 | cmify.getCssById = function getCssById (id) { 97 | return getResultById(id).css 98 | } 99 | 100 | cmify.getAllCss = function getAllCss () { 101 | return cmify.getDependencies() 102 | .map(cmify.getCssById) 103 | .join('\n') 104 | } 105 | 106 | function subCmify (css, filename, parentId, visited) { 107 | const mod = cmify(css, filename, visited) 108 | _depGraph.addDependency(parentId, mod._id) 109 | return mod 110 | } 111 | 112 | function cmify (css, filename, visited) { 113 | // initialize the first time we run 114 | if (!plugins.length) { 115 | cmify.init() 116 | } 117 | 118 | visited = visited || [] 119 | 120 | if (!filename) { filename = caller() } 121 | 122 | const id = filename 123 | const cachedResult = getResultById(id) 124 | if (cachedResult) { 125 | return cachedResult.tokens 126 | } 127 | 128 | // make sure we have a node in the graph 129 | _depGraph.addNode(id) 130 | 131 | // keep track of which modules have been visited in a dependency branch 132 | if (visited.indexOf(id) !== -1) { 133 | const err = new Error('Circular dependency: \n- ' + visited.concat(id).join('\n- ')) 134 | throw err 135 | } 136 | visited.push(id) 137 | 138 | const result = parseCss(css, filename, visited) 139 | 140 | result.tokens._id = id 141 | result.tokens._deps = _depGraph.dependenciesOf(id) 142 | 143 | storeResultById(id, result) 144 | 145 | return result.tokens 146 | } 147 | 148 | // load a css module 149 | cmify.load = function load (filename) { 150 | var fullPath = path.resolve(path.dirname(caller()), filename) 151 | return cmify(fs.readFileSync(fullPath, 'utf8'), fullPath) 152 | } 153 | 154 | cmify.reset = function reset () { 155 | _baseDir = process.cwd() 156 | _resultCache = {} 157 | _depGraph = new DepGraph() 158 | } 159 | 160 | cmify.setBaseDir = function (baseDir) { 161 | _baseDir = baseDir 162 | } 163 | 164 | cmify.init = function init (opts) { 165 | opts = opts || {} 166 | 167 | if (opts.generateScopedName instanceof Function) { 168 | Scope.generateScopedName = (function () { 169 | const orig = Scope.generateScopedName 170 | return opts.generateScopedName(orig) 171 | })() 172 | } else { 173 | generateScopedName() 174 | } 175 | 176 | plugins = [] 177 | .concat(opts.cssBefore || []) 178 | .concat(defaultPlugins) 179 | .concat(opts.cssAfter || []) 180 | } 181 | 182 | // reset once to begin 183 | cmify.reset() 184 | 185 | module.exports = cmify 186 | -------------------------------------------------------------------------------- /node_modules/cool-styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cool-styles", 3 | "style": "./styles.css" 4 | } 5 | -------------------------------------------------------------------------------- /node_modules/cool-styles/styles.css: -------------------------------------------------------------------------------- 1 | .coolness { 2 | border-color: lightblue; 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmify", 3 | "version": "1.6.0", 4 | "description": "A node-first approach to CSS Modules", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "test": "tape tests/index.js | tap-diff" 9 | }, 10 | "keywords": [ 11 | "css-modules", 12 | "postcss" 13 | ], 14 | "author": "joshwnj", 15 | "license": "MIT", 16 | "dependencies": { 17 | "caller": "^1.0.1", 18 | "dependency-graph": "^0.5.0", 19 | "falafel": "^2.0.0", 20 | "mkdirp": "^0.5.1", 21 | "postcss": "^5.0.19", 22 | "postcss-modules-extract-imports": "^1.0.0", 23 | "postcss-modules-local-by-default": "^1.0.1", 24 | "postcss-modules-parser": "^1.1.0", 25 | "postcss-modules-scope": "^1.0.0", 26 | "postcss-modules-values": "^1.1.1", 27 | "resolve": "^1.1.7", 28 | "string-to-stream": "^1.0.1", 29 | "through2": "^2.0.1" 30 | }, 31 | "devDependencies": { 32 | "standard": "^8.0.0", 33 | "tap-diff": "^0.1.1", 34 | "tape": "^4.5.1" 35 | }, 36 | "directories": { 37 | "test": "tests" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/joshwnj/cmify.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/joshwnj/cmify/issues" 45 | }, 46 | "homepage": "https://github.com/joshwnj/cmify#readme" 47 | } 48 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cmify = require('./index') 4 | const through = require('through2') 5 | const falafel = require('falafel') 6 | const str = require('string-to-stream') 7 | const mkdirp = require('mkdirp') 8 | const path = require('path') 9 | const fs = require('fs') 10 | 11 | function createCmStream () { 12 | // fake module 13 | const cmStream = str('module.exports=null') 14 | 15 | // give the stream a filename so browserify can treat it as a real module 16 | cmStream.file = __dirname 17 | cmStream.id = 'cmify' 18 | 19 | return cmStream 20 | } 21 | 22 | function createCmifySource () { 23 | return ` 24 | function cmify () {} 25 | cmify.getAllCss = function () { return ${JSON.stringify(cmify.getAllCss())} } 26 | module.exports = cmify` 27 | } 28 | 29 | function createCssModuleSource (filename) { 30 | const tokens = cmify.load(filename) 31 | 32 | // make sure all dependencies are added to browserify's tree 33 | const output = tokens._deps.map(function (f) { 34 | return 'require("' + f + '")' 35 | }) 36 | 37 | // export the css module's tokens 38 | output.push(`/** last updated: ${Date.now()} **/`) 39 | output.push(`module.exports=${JSON.stringify(tokens)}`) 40 | return output.join('\n') 41 | } 42 | 43 | function cmifyTransform (filename) { 44 | const bufs = [] 45 | let cmifyName = null 46 | 47 | const stream = through(write, end) 48 | return stream 49 | 50 | // ---- 51 | 52 | function write (buf, enc, next) { 53 | bufs.push(buf) 54 | next() 55 | } 56 | 57 | function end (done) { 58 | const src = Buffer.concat(bufs).toString('utf8') 59 | 60 | // handle css files 61 | if (/\.css$/.test(filename)) { 62 | try { 63 | this.push(createCssModuleSource(filename)) 64 | } 65 | catch (err) { 66 | this.emit("error", err); 67 | } 68 | } else { 69 | const ast = falafel(src, { ecmaVersion: 6 }, walk) 70 | this.push(ast.toString()) 71 | } 72 | 73 | this.push(null) 74 | done() 75 | } 76 | 77 | function walk (node) { 78 | // find `require('cmify')` and record the name it's bound to 79 | if (node.type === 'CallExpression' && 80 | node.callee && node.callee.name === 'require' && 81 | node.arguments.length === 1 && 82 | node.arguments[0].value === 'cmify') { 83 | cmifyName = node.parent.id.name 84 | return 85 | } 86 | 87 | // find places where `cmify.load(...)` is called 88 | if (node.type === 'CallExpression' && 89 | node.callee && node.callee.type === 'MemberExpression' && 90 | node.callee.object.name === cmifyName && 91 | node.callee.property.name === 'load' 92 | ) { 93 | // rewrite as `require`, so it gets included in the dependency tree 94 | node.update(`require(${node.arguments[0].raw})`) 95 | } 96 | } 97 | } 98 | 99 | function cmifyPlugin (b, opts) { 100 | opts = opts || {} 101 | 102 | cmify.init({ 103 | cssBefore: opts.cssBefore, 104 | cssAfter: opts.cssAfter 105 | }) 106 | 107 | // register a fake cmify module for the browser 108 | const cmStream = createCmStream() 109 | b.require(cmStream, { expose: cmStream.id }) 110 | 111 | b.transform(cmifyTransform) 112 | 113 | b.on('reset', reset) 114 | reset() 115 | 116 | function reset () { 117 | // add the cmify module 118 | b.pipeline.get('deps').push(through.obj(function write (row, enc, next) { 119 | if (row.id === cmStream.id) { 120 | next(null) 121 | } else { 122 | // css modules need to be regenerated at this point 123 | // (so that imported @value updates are carried through hmr) 124 | if (/\.css$/.test(row.id)) { 125 | cmify.invalidateById(row.id) 126 | try { 127 | row.source = createCssModuleSource(row.id) 128 | } 129 | catch (err) { 130 | this.emit("error", err); 131 | } 132 | } 133 | next(null, row) 134 | } 135 | }, function end (done) { 136 | 137 | const outFile = opts.o || opts.outfile 138 | 139 | if (outFile) { 140 | try { 141 | mkdirp.sync(path.dirname(outFile)) 142 | fs.writeFileSync(outFile, cmify.getAllCss()) 143 | } catch (err) { 144 | this.emit("error", err) 145 | } 146 | } 147 | 148 | const row = { 149 | id: cmStream.id, 150 | source: createCmifySource(), 151 | deps: {}, 152 | file: cmStream.file 153 | } 154 | this.push(row) 155 | 156 | done() 157 | })) 158 | } 159 | 160 | b.on('update', function (files) { 161 | // invalidate cache of any changed css modules 162 | files.forEach(cmify.invalidateById.bind(cmify)) 163 | }) 164 | } 165 | 166 | module.exports = cmifyPlugin 167 | -------------------------------------------------------------------------------- /tests/cases/01-simple/expected.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | styles: { 5 | sky: '_tests_cases_01_simple_styles__sky', 6 | grass: '_tests_cases_01_simple_styles__grass', 7 | _id: path.join(__dirname, 'styles.css'), 8 | _deps: [] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/cases/01-simple/index.js: -------------------------------------------------------------------------------- 1 | const cmify = require('../../../index.js') 2 | const styles = cmify.load('./styles.css') 3 | 4 | module.exports.styles = styles 5 | -------------------------------------------------------------------------------- /tests/cases/01-simple/styles.css: -------------------------------------------------------------------------------- 1 | .sky { 2 | background: blue; 3 | } 4 | 5 | .grass { 6 | background: green; 7 | } -------------------------------------------------------------------------------- /tests/cases/02-compose/expected.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | styles: { 5 | sky: '_tests_cases_02_compose_styles__sky', 6 | grass: '_tests_cases_02_compose_styles__grass _tests_cases_02_compose_things__plants', 7 | _id: path.join(__dirname, 'styles.css'), 8 | _deps: [ 9 | path.join(__dirname, 'things.css') 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/02-compose/index.js: -------------------------------------------------------------------------------- 1 | const cmify = require('../../../index.js') 2 | const styles = cmify.load('./styles.css') 3 | 4 | module.exports.styles = styles 5 | -------------------------------------------------------------------------------- /tests/cases/02-compose/styles.css: -------------------------------------------------------------------------------- 1 | .sky { 2 | background: blue; 3 | } 4 | 5 | .grass { 6 | composes: plants from "./things.css"; 7 | background: green; 8 | } -------------------------------------------------------------------------------- /tests/cases/02-compose/things.css: -------------------------------------------------------------------------------- 1 | .plants { 2 | border: 1px solid green; 3 | } -------------------------------------------------------------------------------- /tests/cases/03-values/expected.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | styles: { 5 | money: '100%', 6 | cashMoney: '50%', 7 | sky: '_tests_cases_03_values_styles__sky', 8 | grass: '_tests_cases_03_values_styles__grass _tests_cases_03_values_things__plants', 9 | _id: path.join(__dirname, 'styles.css'), 10 | _deps: [ 11 | path.join(__dirname, 'things.css') 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/cases/03-values/index.js: -------------------------------------------------------------------------------- 1 | const cmify = require('../../../index.js') 2 | const styles = cmify.load('./styles.css') 3 | 4 | module.exports.styles = styles 5 | -------------------------------------------------------------------------------- /tests/cases/03-values/styles.css: -------------------------------------------------------------------------------- 1 | @value money 100%; 2 | @value money as cashMoney from "./things.css"; 3 | 4 | .sky { 5 | background: blue; 6 | content: money; 7 | } 8 | 9 | .grass { 10 | composes: plants from "./things.css"; 11 | content: cashMoney; 12 | background: green; 13 | } -------------------------------------------------------------------------------- /tests/cases/03-values/things.css: -------------------------------------------------------------------------------- 1 | @value money 50%; 2 | 3 | .plants { 4 | border: 1px solid green; 5 | } -------------------------------------------------------------------------------- /tests/cases/04-compose-from-npm/expected.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | styles: { 5 | sky: '_tests_cases_04_compose_from_npm_styles__sky', 6 | grass: '_tests_cases_04_compose_from_npm_styles__grass _node_modules_cool_styles_styles__coolness', 7 | _id: path.join(__dirname, 'styles.css'), 8 | _deps: [ 9 | path.resolve(path.join(__dirname, '..', '..', '..', 'node_modules', 'cool-styles', 'styles.css')) 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/04-compose-from-npm/index.js: -------------------------------------------------------------------------------- 1 | const cmify = require('../../../index.js') 2 | const styles = cmify.load('./styles.css') 3 | 4 | module.exports.styles = styles 5 | -------------------------------------------------------------------------------- /tests/cases/04-compose-from-npm/styles.css: -------------------------------------------------------------------------------- 1 | .sky { 2 | background: blue; 3 | } 4 | 5 | .grass { 6 | composes: coolness from "cool-styles/styles.css"; 7 | background: green; 8 | } 9 | -------------------------------------------------------------------------------- /tests/cases/05-compose-from-package/expected.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | styles: { 5 | sky: '_tests_cases_05_compose_from_package_styles__sky', 6 | grass: '_tests_cases_05_compose_from_package_styles__grass _node_modules_cool_styles_styles__coolness', 7 | _id: path.join(__dirname, 'styles.css'), 8 | _deps: [ 9 | path.resolve(path.join(__dirname, '..', '..', '..', 'node_modules', 'cool-styles', 'styles.css')) 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/cases/05-compose-from-package/index.js: -------------------------------------------------------------------------------- 1 | const cmify = require('../../../index.js') 2 | const styles = cmify.load('./styles.css') 3 | 4 | module.exports.styles = styles 5 | -------------------------------------------------------------------------------- /tests/cases/05-compose-from-package/styles.css: -------------------------------------------------------------------------------- 1 | .sky { 2 | background: blue; 3 | } 4 | 5 | .grass { 6 | composes: coolness from "cool-styles"; 7 | background: green; 8 | } 9 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const tape = require('tape') 4 | 5 | const casesDir = path.join(__dirname, 'cases') 6 | 7 | fs.readdir(casesDir, function (err, dirs) { 8 | if (err) { throw err } 9 | 10 | dirs.forEach(function (dir) { 11 | const fullDir = path.join(casesDir, dir) 12 | tape(`Case: ${dir}`, function (t) { 13 | const result = require(fullDir) 14 | const expected = require(path.join(fullDir, 'expected.js')) 15 | 16 | t.deepEqual(result, expected, 'Tokens exported as expected') 17 | t.end() 18 | }) 19 | }) 20 | }) 21 | --------------------------------------------------------------------------------