-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding support for CSS modules #38
Changes from 4 commits
d46a749
b2c85ad
6001c18
feedb38
e670622
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 |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
build | ||
node_modules | ||
.github/actions/*/package-lock.json | ||
*.css.d.ts |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,15 +51,16 @@ This style of import can only be used in `static-build`. | |
## CSS | ||
|
||
```js | ||
import cssURL, { inline } from 'css:./styles.css'; | ||
import cssURL, { inline, $tabButton } from './styles.css'; | ||
``` | ||
|
||
`css:` followed by a path to some CSS will add and minify that CSS to the build. | ||
Imports ending `.css` are assumed to be CSS. | ||
|
||
The CSS supports CSS modules, Sass-style nesting, and will be minified. | ||
|
||
- `cssURL` - URL to the CSS resource. | ||
- `inline` - The text of the CSS. | ||
|
||
The CSS can also use Sass-style nesting. | ||
- `$*` - Other imports starting with \$ refer to class names within the CSS. So, if the CSS contains `.tab-button`, then `$tabButton` will be one of the exports. | ||
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. I'm prefixing all these exports with Happy to change the prefix if there's a preference. 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. We should prefix them with 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. Is that a real possibility? (Because I'm all for that) -- but aren't there emoji clashes in Linux? 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. I think technically it should work, but we’re honestly just asking for trouble and it’s a ball-ache to type. 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. reminds me of this from justin w3c/csswg-drafts#3714 |
||
|
||
## Markdown | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,80 +12,121 @@ | |
*/ | ||
import { promises as fsp, readFileSync } from 'fs'; | ||
import { createHash } from 'crypto'; | ||
import { promisify } from 'util'; | ||
import { parse as parsePath, resolve as resolvePath, dirname } from 'path'; | ||
|
||
import postcss from 'postcss'; | ||
import postCSSNested from 'postcss-nested'; | ||
import postCSSUrl from 'postcss-url'; | ||
import postCSSModules from 'postcss-modules'; | ||
import cssNano from 'cssnano'; | ||
import camelCase from 'lodash.camelcase'; | ||
import glob from 'glob'; | ||
|
||
const prefix = 'css:'; | ||
const globP = promisify(glob); | ||
|
||
const suffix = '.css'; | ||
const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g'); | ||
|
||
export default function() { | ||
/** @type {string[]} */ | ||
let emittedCSSIds; | ||
/** @type {Map<string, string>} */ | ||
let hashToId; | ||
/** @type {Map<string, string>} */ | ||
let pathToModule; | ||
|
||
return { | ||
name: 'css', | ||
buildStart() { | ||
async buildStart() { | ||
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. I moved all the CSS processing to |
||
emittedCSSIds = []; | ||
hashToId = new Map(); | ||
}, | ||
async resolveId(id, importer) { | ||
if (!id.startsWith(prefix)) return null; | ||
pathToModule = new Map(); | ||
|
||
const realId = id.slice(prefix.length); | ||
const resolveResult = await this.resolve(realId, importer); | ||
if (!resolveResult) { | ||
throw Error(`Cannot resolve ${resolveResult.id}`); | ||
} | ||
return prefix + resolveResult.id; | ||
}, | ||
async load(id) { | ||
if (!id.startsWith(prefix)) return; | ||
|
||
const realId = id.slice(prefix.length); | ||
const parsedPath = parsePath(realId); | ||
this.addWatchFile(realId); | ||
const file = await fsp.readFile(realId); | ||
const cssResult = await postcss([ | ||
postCSSNested, | ||
postCSSUrl({ | ||
url: ({ relativePath, url }) => { | ||
if (/^https?:\/\//.test(url)) return url; | ||
const parsedPath = parsePath(relativePath); | ||
const source = readFileSync( | ||
resolvePath(dirname(realId), relativePath), | ||
); | ||
const fileId = this.emitFile({ | ||
type: 'asset', | ||
name: parsedPath.base, | ||
source, | ||
}); | ||
const hash = createHash('md5'); | ||
hash.update(source); | ||
const md5 = hash.digest('hex'); | ||
hashToId.set(md5, fileId); | ||
return `/fake/path/to/asset/${md5}/`; | ||
}, | ||
}), | ||
cssNano, | ||
]).process(file, { | ||
from: undefined, | ||
const cssPaths = await globP('static-build/**/*.css', { | ||
nodir: true, | ||
absolute: true, | ||
}); | ||
|
||
const fileId = this.emitFile({ | ||
type: 'asset', | ||
source: cssResult.css, | ||
name: parsedPath.base, | ||
}); | ||
await Promise.all( | ||
cssPaths.map(async path => { | ||
this.addWatchFile(path); | ||
const parsedPath = parsePath(path); | ||
const file = await fsp.readFile(path); | ||
let moduleJSON; | ||
const cssResult = await postcss([ | ||
postCSSNested, | ||
postCSSModules({ | ||
getJSON(_, json) { | ||
moduleJSON = json; | ||
}, | ||
}), | ||
postCSSUrl({ | ||
url: ({ relativePath, url }) => { | ||
if (/^https?:\/\//.test(url)) return url; | ||
const parsedPath = parsePath(relativePath); | ||
const source = readFileSync( | ||
resolvePath(dirname(realId), relativePath), | ||
); | ||
const fileId = this.emitFile({ | ||
type: 'asset', | ||
name: parsedPath.base, | ||
source, | ||
}); | ||
const hash = createHash('md5'); | ||
hash.update(source); | ||
const md5 = hash.digest('hex'); | ||
hashToId.set(md5, fileId); | ||
return `/fake/path/to/asset/${md5}/`; | ||
}, | ||
}), | ||
cssNano, | ||
]).process(file, { | ||
from: undefined, | ||
}); | ||
|
||
const cssClassExports = Object.entries(moduleJSON).map( | ||
([key, val]) => | ||
`export const $${camelCase(key)} = ${JSON.stringify(val)};`, | ||
); | ||
|
||
emittedCSSIds.push(fileId); | ||
const defs = [ | ||
'declare var url: string;', | ||
'export default url;', | ||
'export const inline: string;', | ||
...Object.keys(moduleJSON).map( | ||
key => `export const $${camelCase(key)}: string;`, | ||
), | ||
]; | ||
|
||
const fileId = this.emitFile({ | ||
type: 'asset', | ||
source: cssResult.css, | ||
name: parsedPath.base, | ||
}); | ||
|
||
emittedCSSIds.push(fileId); | ||
|
||
await fsp.writeFile(path + '.d.ts', defs.join('\n')); | ||
|
||
pathToModule.set( | ||
path, | ||
[ | ||
`export default import.meta.ROLLUP_FILE_URL_${fileId}`, | ||
`export const inline = ${JSON.stringify(cssResult.css)}`, | ||
...cssClassExports, | ||
].join('\n'), | ||
); | ||
}), | ||
); | ||
}, | ||
async load(id) { | ||
if (!id.endsWith(suffix)) return; | ||
if (!pathToModule.has(id)) { | ||
throw Error(`Cannot find ${id} in pathToModule`); | ||
} | ||
|
||
return `export default import.meta.ROLLUP_FILE_URL_${fileId}; export const inline = ${JSON.stringify( | ||
cssResult.css, | ||
)}`; | ||
return pathToModule.get(id); | ||
}, | ||
async generateBundle(options, bundle) { | ||
const cssAssets = emittedCSSIds.map(id => this.getFileName(id)); | ||
|
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.
Had to drop the use of
css:
prefix 😢If the import is
css:./styles.css
, there's no way to tell typescript to treat./styles.css
as a relative path. I'll file an issue with 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.
microsoft/TypeScript#37319