ember-scoped-css 0.24.2 → 1.0.0
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 +110 -115
- package/declarations/lib/path/template-transform-paths.d.ts.map +1 -1
- package/declarations/lib/path/utils.d.ts.map +1 -1
- package/declarations/lib/path/utils.paths.test.d.ts +0 -3
- package/declarations/lib/path/utils.paths.test.d.ts.map +1 -1
- package/dist/cjs/babel-plugin.cjs +2 -72
- package/dist/cjs/index.cjs +206 -390
- package/dist/cjs/template-plugin.cjs +201 -120
- package/package.json +5 -18
- package/src/build/babel-plugin.js +2 -38
- package/src/build/index.js +4 -0
- package/src/build/scoped-css-unplugin.js +31 -193
- package/src/build/template-plugin.js +124 -9
- package/src/lib/{rewriteCss.js → css/rewrite.js} +2 -2
- package/src/lib/css/utils.js +74 -3
- package/src/lib/path/hash-from-absolute-path.test.ts +2 -24
- package/src/lib/path/template-transform-paths.js +0 -15
- package/src/lib/path/template-transform-paths.test.ts +8 -69
- package/src/lib/path/utils.appPath.test.ts +4 -15
- package/src/lib/path/utils.findWorkspacePath.test.ts +16 -22
- package/src/lib/path/utils.hashFrom.test.ts +8 -8
- package/src/lib/path/utils.isRelevantFile.test.ts +9 -29
- package/src/lib/path/utils.js +2 -79
- package/src/lib/path/utils.paths.test.ts +1 -4
- package/src/lib/request.js +40 -0
- package/src/lib/rewriteHbs.js +1 -1
- package/dist/cjs/app-css-loader.cjs +0 -531
- package/dist/cjs/ember-classic-support.cjs +0 -708
- package/src/build/app-css-livereload-loader.js +0 -119
- package/src/build/app-css-loader.js +0 -38
- package/src/build/ember-classic-support.js +0 -293
- package/src/lib/findCssInJs.js +0 -29
- package/src/lib/getClassesTagsFromCss.js +0 -48
- package/src/lib/getImportedCssFiles.js +0 -19
- package/src/lib/isInsideGlobal.js +0 -8
- package/src/lib/replaceGlimmerAst.js +0 -63
- package/src/lib/replaceHbsInJs.js +0 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember-scoped-css",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ember-addon"
|
|
@@ -44,17 +44,6 @@
|
|
|
44
44
|
"./helpers/scoped-class": {
|
|
45
45
|
"default": "./classic-app-support/helpers/scoped-class.js"
|
|
46
46
|
},
|
|
47
|
-
"./build/app-css-loader": {
|
|
48
|
-
"import": "./src/build/app-css-loader.js",
|
|
49
|
-
"require": "./dist/cjs/app-css-loader.cjs"
|
|
50
|
-
},
|
|
51
|
-
"./build/app-dependency-loader": {
|
|
52
|
-
"import": "./src/build/app-dependency-loader.js",
|
|
53
|
-
"require": "./dist/cjs/app-dependency-loader.cjs"
|
|
54
|
-
},
|
|
55
|
-
"./build/ember-classic-support": {
|
|
56
|
-
"require": "./dist/cjs/ember-classic-support.cjs"
|
|
57
|
-
},
|
|
58
47
|
"./addon-main.cjs": "./addon-main.cjs",
|
|
59
48
|
"./babel-plugin": {
|
|
60
49
|
"import": "./src/build/babel-plugin.js",
|
|
@@ -83,17 +72,15 @@
|
|
|
83
72
|
},
|
|
84
73
|
"devDependencies": {
|
|
85
74
|
"@babel/eslint-parser": "^7.26.8",
|
|
86
|
-
"@nullvoxpopuli/eslint-configs": "^
|
|
75
|
+
"@nullvoxpopuli/eslint-configs": "^5.3.4",
|
|
87
76
|
"@tsconfig/ember": "^3.0.9",
|
|
88
77
|
"@tsconfig/strictest": "^2.0.5",
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
|
90
|
-
"@typescript-eslint/parser": "^8.24.1",
|
|
91
78
|
"concurrently": "^9.1.2",
|
|
92
79
|
"ember-template-lint": "^6.1.0",
|
|
93
80
|
"esbuild": "^0.25.0",
|
|
94
81
|
"esbuild-plugin-vitest-cleaner": "^0.5.1",
|
|
95
|
-
"eslint": "^
|
|
96
|
-
"prettier": "^3.
|
|
82
|
+
"eslint": "^9.36.0",
|
|
83
|
+
"prettier": "^3.6.2",
|
|
97
84
|
"typescript": "^5.2.2",
|
|
98
85
|
"vitest": "^3.0.6",
|
|
99
86
|
"webpack": "^5.98.0"
|
|
@@ -107,7 +94,7 @@
|
|
|
107
94
|
}
|
|
108
95
|
},
|
|
109
96
|
"engines": {
|
|
110
|
-
"node": ">=
|
|
97
|
+
"node": ">= 22.16"
|
|
111
98
|
},
|
|
112
99
|
"peerDependencies": {
|
|
113
100
|
"ember-template-lint": ">= 5.7.2",
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import nodePath from 'path';
|
|
4
|
-
|
|
5
|
-
import { cssPathFor, isRelevantFile } from '../lib/path/utils.js';
|
|
1
|
+
import { isRelevantFile } from '../lib/path/utils.js';
|
|
6
2
|
|
|
7
3
|
function _isRelevantFile(state, cwd) {
|
|
8
4
|
let fileName = state.file.opts.filename;
|
|
@@ -21,11 +17,10 @@ function _isRelevantFile(state, cwd) {
|
|
|
21
17
|
*/
|
|
22
18
|
export default (env, options, workingDirectory) => {
|
|
23
19
|
/**
|
|
24
|
-
* This babel plugin does
|
|
20
|
+
* This babel plugin does two things:
|
|
25
21
|
* - removes the import of scopedClass, if it exists
|
|
26
22
|
* - if scopedClass was imported, it is removed from any component's "scope bag"
|
|
27
23
|
* (the scope bag being a low-level object used for passing what is "in scope" for a component)
|
|
28
|
-
* - adds an import to the CSS file, if it exists
|
|
29
24
|
*/
|
|
30
25
|
return {
|
|
31
26
|
visitor: {
|
|
@@ -36,8 +31,6 @@ export default (env, options, workingDirectory) => {
|
|
|
36
31
|
|
|
37
32
|
return;
|
|
38
33
|
}
|
|
39
|
-
|
|
40
|
-
state.importUtil = new ImportUtil(env, path);
|
|
41
34
|
},
|
|
42
35
|
},
|
|
43
36
|
ImportDeclaration(path, state) {
|
|
@@ -76,35 +69,6 @@ export default (env, options, workingDirectory) => {
|
|
|
76
69
|
path.remove();
|
|
77
70
|
}
|
|
78
71
|
},
|
|
79
|
-
/**
|
|
80
|
-
* If there is a CSS file, AND a corresponding template,
|
|
81
|
-
* we can import the CSS to then defer to the CSS loader
|
|
82
|
-
* or other CSS processing to handle the postfixing
|
|
83
|
-
*/
|
|
84
|
-
CallExpression(path, state) {
|
|
85
|
-
if (state.canSkip) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const node = path.node;
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
node.callee.name === 'precompileTemplate' ||
|
|
93
|
-
node.callee.name === 'hbs' ||
|
|
94
|
-
node.callee.name === 'createTemplateFactory'
|
|
95
|
-
) {
|
|
96
|
-
const fileName =
|
|
97
|
-
state.file.opts.sourceFileName || state.file.opts.filename;
|
|
98
|
-
|
|
99
|
-
let cssPath = cssPathFor(fileName);
|
|
100
|
-
|
|
101
|
-
if (existsSync(cssPath)) {
|
|
102
|
-
let baseCSS = nodePath.basename(cssPath);
|
|
103
|
-
|
|
104
|
-
state.importUtil.importForSideEffect(`./${baseCSS}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
72
|
},
|
|
109
73
|
};
|
|
110
74
|
};
|
package/src/build/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { default as babelPlugin } from './babel-plugin.js';
|
|
2
2
|
export { default as scopedCssUnplugin } from './scoped-css-unplugin.js';
|
|
3
3
|
export { createPlugin as templatePlugin } from './template-plugin.js';
|
|
4
|
+
|
|
5
|
+
import { default as scopedCssUnplugin } from './scoped-css-unplugin.js';
|
|
6
|
+
|
|
7
|
+
export const rollupPlugin = scopedCssUnplugin.rollup;
|
|
@@ -1,201 +1,39 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
1
|
import path from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
2
|
|
|
6
3
|
import { createUnplugin } from 'unplugin';
|
|
7
4
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import rewriteHbs from '../lib/rewriteHbs.js';
|
|
17
|
-
|
|
18
|
-
function isJsFile(id) {
|
|
19
|
-
return (
|
|
20
|
-
id.endsWith('.js') ||
|
|
21
|
-
id.endsWith('.ts') ||
|
|
22
|
-
id.endsWith('.gjs') ||
|
|
23
|
-
id.endsWith('.gts')
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isHbsFile(id) {
|
|
28
|
-
return id.endsWith('.hbs');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isCssFile(id) {
|
|
32
|
-
return id.endsWith('.css');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function asCSSPath(id) {
|
|
36
|
-
const cssPath = id.endsWith('.hbs')
|
|
37
|
-
? id.replace(/\.hbs$/, '.css')
|
|
38
|
-
: id.replace(/(\.hbs)?\.(js|ts|gjs|gts)$/, '.css');
|
|
39
|
-
|
|
40
|
-
return cssPath;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function transformJsFile(code, id) {
|
|
44
|
-
let cssPath = asCSSPath(id);
|
|
45
|
-
let cssFileName = path.basename(cssPath);
|
|
46
|
-
|
|
47
|
-
let cssExists = existsSync(cssPath);
|
|
48
|
-
|
|
49
|
-
// Check for pods (using styles.css)
|
|
50
|
-
if (!cssExists) {
|
|
51
|
-
let [, ...parts] = id.split('/').reverse();
|
|
52
|
-
|
|
53
|
-
cssPath = [...parts.reverse(), 'styles.css'].join('/');
|
|
54
|
-
cssFileName = 'styles.css';
|
|
55
|
-
cssExists = existsSync(cssPath);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let css;
|
|
59
|
-
|
|
60
|
-
if (cssExists) {
|
|
61
|
-
css = await readFile(cssPath, 'utf8');
|
|
62
|
-
} else {
|
|
63
|
-
return {
|
|
64
|
-
code,
|
|
65
|
-
map: null,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// add css import for js and gjs files
|
|
70
|
-
code = `import './${cssFileName}';\n\n${code}`;
|
|
71
|
-
|
|
72
|
-
// rewrite hbs in js in case it is gjs file (for gjs files hbs is already in js file)
|
|
73
|
-
|
|
74
|
-
const rewrittenCode = replaceHbsInJs(code, (hbs, scopedClass) => {
|
|
75
|
-
const { classes, tags } = getClassesTagsFromCss(css);
|
|
76
|
-
const postfix = hashFromAbsolutePath(cssPath);
|
|
77
|
-
const rewritten = rewriteHbs(hbs, classes, tags, postfix, scopedClass);
|
|
78
|
-
|
|
79
|
-
return rewritten;
|
|
80
|
-
});
|
|
81
|
-
|
|
5
|
+
import { decodeScopedCSSRequest, isScopedCSSRequest } from '../lib/request.js';
|
|
6
|
+
/**
|
|
7
|
+
* The plugin that handles CSS requests from other transform (e.g.: babel)
|
|
8
|
+
*
|
|
9
|
+
* This plugin takes no options because the CSS transform has already happened by the time
|
|
10
|
+
* we handle it here.
|
|
11
|
+
*/
|
|
12
|
+
export default createUnplugin(() => {
|
|
82
13
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function gatherCSSFiles(bundle) {
|
|
99
|
-
let cssFiles = [];
|
|
100
|
-
|
|
101
|
-
for (let asset in bundle) {
|
|
102
|
-
const cssAsset = asset.replace('js', 'css');
|
|
103
|
-
|
|
104
|
-
if (!asset.endsWith('js') || !bundle[cssAsset]) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (process.env.environment === 'development') {
|
|
109
|
-
cssFiles.push(bundle[cssAsset].source);
|
|
110
|
-
delete bundle[cssAsset];
|
|
111
|
-
} else {
|
|
112
|
-
const cssImport = path.basename(asset.replace('.js', '.css'));
|
|
113
|
-
const importLine = `import './${cssImport}';`;
|
|
114
|
-
|
|
115
|
-
const code = bundle[asset].code;
|
|
116
|
-
|
|
117
|
-
// add import to js files
|
|
118
|
-
if (code && code.indexOf(importLine) < 0) {
|
|
119
|
-
bundle[asset].code = `${importLine}\n` + code;
|
|
14
|
+
name: 'ember-scoped-css-unplugin',
|
|
15
|
+
|
|
16
|
+
resolveId(id, importer) {
|
|
17
|
+
if (isScopedCSSRequest(id)) {
|
|
18
|
+
let parsed = decodeScopedCSSRequest(id);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: path.resolve(path.dirname(importer), parsed.postfix + '.css'),
|
|
22
|
+
meta: {
|
|
23
|
+
'scoped-css': {
|
|
24
|
+
css: parsed.css,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
120
28
|
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return cssFiles;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export default createUnplugin(
|
|
128
|
-
/**
|
|
129
|
-
* @typedef {object} Options
|
|
130
|
-
* @property {string} [layerName] the name of the layer to place the generated css. Defaults to "components"
|
|
131
|
-
*
|
|
132
|
-
* @param {Options} [options]
|
|
133
|
-
*/
|
|
134
|
-
(options) => {
|
|
135
|
-
let cwd = process.cwd();
|
|
136
|
-
let additionalRoots = options?.additionalRoots || [];
|
|
29
|
+
},
|
|
137
30
|
|
|
138
|
-
|
|
139
|
-
|
|
31
|
+
load(id) {
|
|
32
|
+
let meta = this.getModuleInfo(id)?.meta?.['scoped-css'];
|
|
140
33
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
fileName: 'scoped.css',
|
|
148
|
-
source: cssFiles.join('\n'),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
vite: {
|
|
153
|
-
generateBundle() {
|
|
154
|
-
/* deliberately do nothing */
|
|
155
|
-
},
|
|
156
|
-
transform(code, jsPath) {
|
|
157
|
-
if (!isRelevantFile(jsPath, { additionalRoots, cwd })) return;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* HBS files are actually JS files with a call to precompileTemplate
|
|
161
|
-
*/
|
|
162
|
-
if (isHbsFile(jsPath)) {
|
|
163
|
-
return transformJsFile(code, jsPath);
|
|
164
|
-
} else if (isJsFile(jsPath)) {
|
|
165
|
-
return transformJsFile(code, jsPath);
|
|
166
|
-
} else if (isCssFile(jsPath)) {
|
|
167
|
-
return transformCssFile(code, jsPath, options?.layerName);
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
transform(code, jsPath) {
|
|
173
|
-
if (!isRelevantFile(jsPath, { additionalRoots, cwd })) return;
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* HBS files are actually JS files with a call to precompileTemplate
|
|
177
|
-
*/
|
|
178
|
-
if (isHbsFile(jsPath)) {
|
|
179
|
-
return transformJsFile(code, jsPath);
|
|
180
|
-
} else if (isJsFile(jsPath)) {
|
|
181
|
-
return transformJsFile(code, jsPath);
|
|
182
|
-
} else if (isCssFile(jsPath)) {
|
|
183
|
-
let css = transformCssFile(code, jsPath, options?.layerName);
|
|
184
|
-
|
|
185
|
-
const emittedFileName = jsPath.replace(
|
|
186
|
-
path.join(process.cwd(), 'src/'),
|
|
187
|
-
'',
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
this.emitFile({
|
|
191
|
-
type: 'asset',
|
|
192
|
-
fileName: emittedFileName,
|
|
193
|
-
source: css,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return '';
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
},
|
|
201
|
-
);
|
|
34
|
+
if (meta) {
|
|
35
|
+
return meta.css;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { rewriteCss } from '../lib/css/rewrite.js';
|
|
8
|
+
import { getCSSContentInfo, getCSSInfo } from '../lib/css/utils.js';
|
|
8
9
|
import { fixFilename } from '../lib/path/template-transform-paths.js';
|
|
9
10
|
import {
|
|
10
11
|
appPath,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
hashFromModulePath,
|
|
13
14
|
isRelevantFile,
|
|
14
15
|
} from '../lib/path/utils.js';
|
|
16
|
+
import { makeRequest } from '../lib/request.js';
|
|
15
17
|
import { templatePlugin } from '../lib/rewriteHbs.js';
|
|
16
18
|
|
|
17
19
|
const noopPlugin = {
|
|
@@ -28,9 +30,10 @@ export function createPlugin(config) {
|
|
|
28
30
|
* @param {ASTPluginEnvironment} env
|
|
29
31
|
*/
|
|
30
32
|
return function scopedCss(env) {
|
|
33
|
+
let cwd = process.cwd();
|
|
31
34
|
let isRelevant = isRelevantFile(env.filename, {
|
|
32
35
|
additionalRoots: config.additionalRoots,
|
|
33
|
-
cwd
|
|
36
|
+
cwd,
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
if (!isRelevant) {
|
|
@@ -39,18 +42,64 @@ export function createPlugin(config) {
|
|
|
39
42
|
|
|
40
43
|
let absolutePath = fixFilename(env.filename);
|
|
41
44
|
let modulePath = appPath(absolutePath);
|
|
45
|
+
let postfix = hashFromModulePath(modulePath);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The list of naked tag selectors found in the CSS
|
|
49
|
+
*
|
|
50
|
+
* @type {Set<string>}
|
|
51
|
+
*/
|
|
52
|
+
let scopedTags = new Set();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The list of classes found in the CSS
|
|
56
|
+
*
|
|
57
|
+
* @type {Set<string>}
|
|
58
|
+
*/
|
|
59
|
+
let scopedClasses = new Set();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {{ tags: Set<string>; classes: Set<string> }} info
|
|
63
|
+
*/
|
|
64
|
+
function addInfo(info) {
|
|
65
|
+
for (let item of info.tags) {
|
|
66
|
+
scopedTags.add(item);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let item of info.classes) {
|
|
70
|
+
scopedClasses.add(item);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
42
73
|
|
|
43
74
|
let cssPath = cssPathFor(absolutePath);
|
|
44
75
|
let info = getCSSInfo(cssPath);
|
|
45
|
-
let postfix = hashFromModulePath(modulePath);
|
|
46
76
|
|
|
47
|
-
|
|
48
|
-
|
|
77
|
+
/**
|
|
78
|
+
* This will be falsey if we don't have a co-located CSS file.
|
|
79
|
+
* We'll still want to check for embedded <style scoped> tags though.
|
|
80
|
+
*/
|
|
81
|
+
if (info) {
|
|
82
|
+
addInfo(info);
|
|
83
|
+
|
|
84
|
+
let localCssPath = cssPath.replace(cwd + '/', '');
|
|
85
|
+
let scopedCss = rewriteCss(
|
|
86
|
+
info.css,
|
|
87
|
+
postfix,
|
|
88
|
+
localCssPath,
|
|
89
|
+
config.layerName,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
let cssRequest = makeRequest(postfix, info.id, scopedCss);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* With this we don't need a JS plugin
|
|
96
|
+
*/
|
|
97
|
+
env.meta.jsutils.importForSideEffect(cssRequest);
|
|
49
98
|
}
|
|
50
99
|
|
|
51
100
|
let visitors = templatePlugin({
|
|
52
|
-
classes:
|
|
53
|
-
tags:
|
|
101
|
+
classes: scopedClasses,
|
|
102
|
+
tags: scopedTags,
|
|
54
103
|
postfix,
|
|
55
104
|
});
|
|
56
105
|
|
|
@@ -59,13 +108,58 @@ export function createPlugin(config) {
|
|
|
59
108
|
visitor: {
|
|
60
109
|
// Stack Manager
|
|
61
110
|
...visitors,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* We have to eagerly get the <style scoped> contents, so we can pre-parse
|
|
114
|
+
* the tags and classes to then pass to the other visitors so that they can
|
|
115
|
+
* appropriately change matching classes / tags.
|
|
116
|
+
*/
|
|
117
|
+
Template(node) {
|
|
118
|
+
/**
|
|
119
|
+
* We only allow a scoped <style> at the root
|
|
120
|
+
*/
|
|
121
|
+
let styleTag = node.body.find(
|
|
122
|
+
(n) => n.type === 'ElementNode' && n.tag === 'style',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (hasScopedAttribute(styleTag)) {
|
|
126
|
+
let css = textContent(styleTag);
|
|
127
|
+
let info = getCSSContentInfo(css);
|
|
128
|
+
let localCssPath = `<inline>/` + cssPath.replace(cwd + '/', '');
|
|
129
|
+
let scopedCss = rewriteCss(
|
|
130
|
+
info.css,
|
|
131
|
+
postfix,
|
|
132
|
+
localCssPath,
|
|
133
|
+
config.layerName,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
addInfo(info);
|
|
137
|
+
|
|
138
|
+
let cssRequest = makeRequest(postfix, info.id, scopedCss);
|
|
139
|
+
|
|
140
|
+
env.meta.jsutils.importForSideEffect(cssRequest);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
62
144
|
// Visitors broken out like this so we can conditionally
|
|
63
145
|
// debug based on file path.
|
|
64
146
|
AttrNode(...args) {
|
|
65
147
|
return visitors.AttrNode(...args);
|
|
66
148
|
},
|
|
67
|
-
ElementNode(
|
|
68
|
-
|
|
149
|
+
ElementNode(node, walker) {
|
|
150
|
+
// class attribute handling
|
|
151
|
+
visitors.ElementNode(node, walker);
|
|
152
|
+
|
|
153
|
+
if (hasScopedAttribute(node)) {
|
|
154
|
+
if (walker.parent?.node.type !== 'Template') {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'<style scoped> tags must be at the root of the template, they cannot be nested',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Returning null removes the node
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
69
163
|
},
|
|
70
164
|
MustacheStatement(...args) {
|
|
71
165
|
return visitors.MustacheStatement(...args);
|
|
@@ -77,3 +171,24 @@ export function createPlugin(config) {
|
|
|
77
171
|
};
|
|
78
172
|
};
|
|
79
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Thanks, CardStack and @ef4 for this code.
|
|
177
|
+
*/
|
|
178
|
+
const SCOPED_ATTRIBUTE_NAME = 'scoped';
|
|
179
|
+
|
|
180
|
+
function hasScopedAttribute(node) {
|
|
181
|
+
if (!node) return;
|
|
182
|
+
if (node.tag !== 'style') return;
|
|
183
|
+
if (node.type !== 'ElementNode') return;
|
|
184
|
+
|
|
185
|
+
return node.attributes.some(
|
|
186
|
+
(attribute) => attribute.name === SCOPED_ATTRIBUTE_NAME,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function textContent(node) {
|
|
191
|
+
let textChildren = node.children.filter((c) => c.type === 'TextNode');
|
|
192
|
+
|
|
193
|
+
return textChildren.map((c) => c.chars).join('');
|
|
194
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import postcss from 'postcss';
|
|
2
2
|
import parser from 'postcss-selector-parser';
|
|
3
3
|
|
|
4
|
-
import isInsideGlobal from './
|
|
4
|
+
import { isInsideGlobal } from './utils.js';
|
|
5
5
|
|
|
6
6
|
function rewriteSelector(sel, postfix) {
|
|
7
7
|
const transform = (selectors) => {
|
|
@@ -37,7 +37,7 @@ function isInsideKeyframes(node) {
|
|
|
37
37
|
return isInsideKeyframes(parent);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
40
|
+
export function rewriteCss(css, postfix, fileName, layerName) {
|
|
41
41
|
const layerNameWithDefault = layerName ?? 'components';
|
|
42
42
|
const ast = postcss.parse(css);
|
|
43
43
|
|
package/src/lib/css/utils.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import postcss from 'postcss';
|
|
3
|
+
import parser from 'postcss-selector-parser';
|
|
2
4
|
|
|
3
|
-
import
|
|
5
|
+
import { md5 } from '../path/md5.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} css
|
|
9
|
+
* @return {string} hashed down version of the CSS for disambiguating
|
|
10
|
+
*/
|
|
11
|
+
export function hash(css) {
|
|
12
|
+
return `css-${md5(css)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isInsideGlobal(node, func) {
|
|
16
|
+
const parent = node.parent;
|
|
17
|
+
|
|
18
|
+
if (!parent) return false;
|
|
19
|
+
if (parent.type === 'pseudo' && parent.value === ':global') return true;
|
|
20
|
+
|
|
21
|
+
return isInsideGlobal(parent, func);
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
/**
|
|
6
25
|
* @param {string} cssPath path to a CSS file
|
|
@@ -11,7 +30,59 @@ export function getCSSInfo(cssPath) {
|
|
|
11
30
|
}
|
|
12
31
|
|
|
13
32
|
let css = readFileSync(cssPath, 'utf8');
|
|
14
|
-
let result = getClassesTagsFromCss(css);
|
|
15
33
|
|
|
16
|
-
return
|
|
34
|
+
return getCSSContentInfo(css);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* We use this function to check each class used in the template
|
|
39
|
+
* to see if we need to leave it alone or transform it
|
|
40
|
+
*
|
|
41
|
+
* @param {string} css the CSS's contents
|
|
42
|
+
* @return {{ classes: Set<string>, tags: Set<string>, css: string, id: string }}
|
|
43
|
+
*/
|
|
44
|
+
export function getCSSContentInfo(css) {
|
|
45
|
+
const classes = new Set();
|
|
46
|
+
const tags = new Set();
|
|
47
|
+
|
|
48
|
+
const ast = postcss.parse(css);
|
|
49
|
+
|
|
50
|
+
ast.walk((node) => {
|
|
51
|
+
if (node.type === 'rule') {
|
|
52
|
+
getClassesAndTags(node.selector, classes, tags);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let id = hash(css);
|
|
57
|
+
|
|
58
|
+
return { classes, tags, css, id };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getClassesAndTags(sel, classes, tags) {
|
|
62
|
+
const transform = (sls) => {
|
|
63
|
+
sls.walk((selector) => {
|
|
64
|
+
if (selector.type === 'class' && !isInsideGlobal(selector)) {
|
|
65
|
+
classes.add(selector.value);
|
|
66
|
+
} else if (selector.type === 'tag' && !isInsideGlobal(selector)) {
|
|
67
|
+
tags.add(selector.value);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
parser(transform).processSync(sel);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (import.meta.vitest) {
|
|
76
|
+
const { it, expect } = import.meta.vitest;
|
|
77
|
+
|
|
78
|
+
it('should return classes and tags that are not in :global', function () {
|
|
79
|
+
const css = '.baz :global(.foo) .bar div :global(p) { color: red; }';
|
|
80
|
+
const { classes, tags } = getCSSContentInfo(css);
|
|
81
|
+
|
|
82
|
+
// classes should be baz and bar
|
|
83
|
+
expect(classes.size).to.equal(2);
|
|
84
|
+
expect([...classes]).to.have.members(['baz', 'bar']);
|
|
85
|
+
expect(tags.size).to.equal(1);
|
|
86
|
+
expect([...tags]).to.have.members(['div']);
|
|
87
|
+
});
|
|
17
88
|
}
|
|
@@ -3,37 +3,15 @@ import path from 'node:path';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { hashFromAbsolutePath } from './hash-from-absolute-path.js';
|
|
6
|
-
import { hashFromModulePath } from './hash-from-module-path.js';
|
|
7
6
|
import { paths } from './utils.paths.test.js';
|
|
8
7
|
|
|
9
8
|
describe('hashFromAbsolutePath', () => {
|
|
10
9
|
describe(`the module: "embroider-app/templates/application"`, () => {
|
|
11
|
-
let file = 'templates/application';
|
|
12
|
-
let expected = 'ea418816b';
|
|
13
|
-
|
|
14
|
-
it('matches the module path', () => {
|
|
15
|
-
let postfix = hashFromModulePath(`embroider-app/${file}`);
|
|
16
|
-
|
|
17
|
-
expect(postfix).to.equal(expected);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('works with rewritten app', () => {
|
|
21
|
-
let filePath = path.join(
|
|
22
|
-
paths.embroiderApp,
|
|
23
|
-
'/node_modules/.embroider/rewritten-app',
|
|
24
|
-
file,
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
let postfix = hashFromAbsolutePath(filePath);
|
|
28
|
-
|
|
29
|
-
expect(postfix).to.equal(expected);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
10
|
it('works with direct path', () => {
|
|
33
|
-
let filePath = path.join(paths.
|
|
11
|
+
let filePath = path.join(paths.viteApp, 'src/components/third');
|
|
34
12
|
let postfix = hashFromAbsolutePath(filePath);
|
|
35
13
|
|
|
36
|
-
expect(postfix).to.equal(
|
|
14
|
+
expect(postfix).to.equal('e4b5bd4dc');
|
|
37
15
|
});
|
|
38
16
|
});
|
|
39
17
|
});
|