@astryxdesign/build 0.0.0-bootstrap.0 → 0.0.15
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/LICENSE +21 -0
- package/README.md +245 -1
- package/dist/vite.mjs +284 -0
- package/package.json +54 -5
- package/src/babel.js +106 -0
- package/src/babel.test.mjs +73 -0
- package/src/config.js +92 -0
- package/src/index.js +251 -0
- package/src/next.js +66 -0
- package/src/vite.test.ts +45 -0
- package/src/vite.ts +427 -0
package/src/babel.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @astryxdesign/build/babel
|
|
7
|
+
*
|
|
8
|
+
* Babel plugin that delegates to @stylexjs/babel-plugin with a
|
|
9
|
+
* different classNamePrefix for Astryx library files vs product files.
|
|
10
|
+
*
|
|
11
|
+
* Library files get 'astryx' prefix (.astryx78zum5), product files get 'x' (.x78zum5).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const LIBRARY_PATTERNS = [
|
|
15
|
+
'packages/core/',
|
|
16
|
+
'packages/themes/',
|
|
17
|
+
'packages/lab/',
|
|
18
|
+
'node_modules/@astryxdesign/',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
module.exports = function xdsBabelPlugin(api, options) {
|
|
22
|
+
const stylexPlugin = require('@stylexjs/babel-plugin');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
libraryPatterns = LIBRARY_PATTERNS,
|
|
26
|
+
libraryPrefix = 'astryx',
|
|
27
|
+
classNamePrefix = 'x',
|
|
28
|
+
...stylexOptions
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
// Build the two sets of options — only classNamePrefix differs
|
|
32
|
+
const libraryOpts = {...stylexOptions, classNamePrefix: libraryPrefix};
|
|
33
|
+
const productOpts = {...stylexOptions, classNamePrefix};
|
|
34
|
+
|
|
35
|
+
// Create two plugin instances
|
|
36
|
+
const libraryPlugin = stylexPlugin(api, libraryOpts);
|
|
37
|
+
const productPlugin = stylexPlugin(api, productOpts);
|
|
38
|
+
|
|
39
|
+
function getPluginAndOpts(filename) {
|
|
40
|
+
const isLibrary = libraryPatterns.some(p => filename.includes(p));
|
|
41
|
+
return isLibrary
|
|
42
|
+
? {plugin: libraryPlugin, opts: libraryOpts}
|
|
43
|
+
: {plugin: productPlugin, opts: productOpts};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Helper: call a visitor function with overridden state.opts
|
|
47
|
+
function callVisitor(visitor, key, path, state) {
|
|
48
|
+
const {plugin, opts} = getPluginAndOpts(state.filename || '');
|
|
49
|
+
const v = plugin.visitor[key];
|
|
50
|
+
if (!v) return;
|
|
51
|
+
|
|
52
|
+
// Override state.opts so StyleX reads the correct classNamePrefix
|
|
53
|
+
const originalOpts = state.opts;
|
|
54
|
+
state.opts = opts;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (typeof v === 'function') {
|
|
58
|
+
v.call(this, path, state);
|
|
59
|
+
} else if (typeof v === 'object') {
|
|
60
|
+
// enter/exit form — should not happen at top level for stylex
|
|
61
|
+
// but handle it anyway
|
|
62
|
+
if (visitor === 'enter' && v.enter) v.enter.call(this, path, state);
|
|
63
|
+
if (visitor === 'exit' && v.exit) v.exit.call(this, path, state);
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
state.opts = originalOpts;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get all visitor keys
|
|
71
|
+
const allKeys = new Set([
|
|
72
|
+
...Object.keys(libraryPlugin.visitor || {}),
|
|
73
|
+
...Object.keys(productPlugin.visitor || {}),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const visitor = {};
|
|
77
|
+
|
|
78
|
+
for (const key of allKeys) {
|
|
79
|
+
const sample = libraryPlugin.visitor[key] || productPlugin.visitor[key];
|
|
80
|
+
|
|
81
|
+
if (typeof sample === 'object' && (sample.enter || sample.exit)) {
|
|
82
|
+
// enter/exit form (Program uses this)
|
|
83
|
+
visitor[key] = {};
|
|
84
|
+
if (sample.enter) {
|
|
85
|
+
visitor[key].enter = function(path, state) {
|
|
86
|
+
callVisitor.call(this, 'enter', key, path, state);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (sample.exit) {
|
|
90
|
+
visitor[key].exit = function(path, state) {
|
|
91
|
+
callVisitor.call(this, 'exit', key, path, state);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Simple function visitor
|
|
96
|
+
visitor[key] = function(path, state) {
|
|
97
|
+
callVisitor.call(this, 'enter', key, path, state);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
name: 'xds-babel-plugin',
|
|
104
|
+
visitor,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file babel.test.mjs
|
|
5
|
+
* @description Verifies that the XDS babel wrapper applies the configured
|
|
6
|
+
* library StyleX class-name prefix to XDS library files. Part of the
|
|
7
|
+
* the library atom prefix defaults to `astryx` and is configurable
|
|
8
|
+
* before the final cutover.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect} from 'vitest';
|
|
12
|
+
import {createRequire} from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const babel = require('@babel/core');
|
|
16
|
+
const xdsBabelPlugin = require('./babel.js');
|
|
17
|
+
|
|
18
|
+
const SOURCE = `
|
|
19
|
+
import * as stylex from '@stylexjs/stylex';
|
|
20
|
+
export const styles = stylex.create({
|
|
21
|
+
box: {color: 'red'},
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Transform a StyleX source through the XDS babel wrapper as if it were a
|
|
27
|
+
* library file, returning the emitted code. `libraryPrefix` controls the
|
|
28
|
+
* atomic class-name prefix for library files.
|
|
29
|
+
*/
|
|
30
|
+
function transformLibraryFile(libraryPrefix) {
|
|
31
|
+
const result = babel.transformSync(SOURCE, {
|
|
32
|
+
// A path matching one of the library patterns so the wrapper routes it
|
|
33
|
+
// through the library plugin instance.
|
|
34
|
+
filename: 'node_modules/@astryxdesign/core/src/Box/XDSBox.tsx',
|
|
35
|
+
babelrc: false,
|
|
36
|
+
configFile: false,
|
|
37
|
+
plugins: [
|
|
38
|
+
[
|
|
39
|
+
xdsBabelPlugin,
|
|
40
|
+
{
|
|
41
|
+
...(libraryPrefix ? {libraryPrefix} : {}),
|
|
42
|
+
unstable_moduleResolution: {type: 'commonJS', rootDir: process.cwd()},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
return result?.code ?? '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Extract the StyleX atomic class names referenced in the emitted code. */
|
|
51
|
+
function atomicClasses(code) {
|
|
52
|
+
// StyleX emits atoms like "xds1a2b3c" / "astryx1a2b3c" in the compiled output.
|
|
53
|
+
return code.match(/\b(?:xds|astryx|lib)[a-z0-9]{4,}\b/g) ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('xds babel wrapper -- library StyleX prefix', () => {
|
|
57
|
+
it('defaults library atoms to the `astryx` prefix', () => {
|
|
58
|
+
const code = transformLibraryFile(undefined);
|
|
59
|
+
const atoms = atomicClasses(code);
|
|
60
|
+
expect(atoms.length).toBeGreaterThan(0);
|
|
61
|
+
expect(atoms.every(c => c.startsWith('astryx'))).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
atoms.some(c => c.startsWith('xds') && !c.startsWith('astryx')),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses a configured custom prefix for library atoms', () => {
|
|
68
|
+
const code = transformLibraryFile('lib');
|
|
69
|
+
const atoms = atomicClasses(code);
|
|
70
|
+
expect(atoms.length).toBeGreaterThan(0);
|
|
71
|
+
expect(atoms.every(c => c.startsWith('lib'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @astryxdesign/build
|
|
7
|
+
*
|
|
8
|
+
* Unified build configuration for Astryx source builds.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* // babel.config.js
|
|
12
|
+
* const {babel} = require('@astryxdesign/build');
|
|
13
|
+
* module.exports = babel(__dirname);
|
|
14
|
+
*
|
|
15
|
+
* // postcss.config.js
|
|
16
|
+
* const {postcss} = require('@astryxdesign/build');
|
|
17
|
+
* module.exports = postcss(__dirname);
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve Astryx package aliases from a root directory.
|
|
24
|
+
* Handles both npm installs (node_modules/@astryxdesign/core) and
|
|
25
|
+
* monorepo layouts (packages/core).
|
|
26
|
+
*/
|
|
27
|
+
function resolveAliases(rootDir) {
|
|
28
|
+
const coreDir = path.join(rootDir, 'node_modules/@astryxdesign/core');
|
|
29
|
+
return {
|
|
30
|
+
'@astryxdesign/core/*': [path.join(coreDir, '*')],
|
|
31
|
+
'@astryxdesign/core': [coreDir],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build shared StyleX babel plugin options from a root directory.
|
|
37
|
+
*/
|
|
38
|
+
function stylexOptions(rootDir, overrides = {}) {
|
|
39
|
+
return {
|
|
40
|
+
dev: process.env.NODE_ENV !== 'production',
|
|
41
|
+
runtimeInjection: false,
|
|
42
|
+
enableInlinedConditionalMerge: true,
|
|
43
|
+
treeshakeCompensation: true,
|
|
44
|
+
aliases: resolveAliases(rootDir),
|
|
45
|
+
unstable_moduleResolution: {
|
|
46
|
+
type: 'commonJS',
|
|
47
|
+
},
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a complete babel.config.js for Astryx source builds.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} rootDir — __dirname of the project root
|
|
56
|
+
* @param {object} [overrides] — extra StyleX options to merge
|
|
57
|
+
* @returns {object} babel config object
|
|
58
|
+
*/
|
|
59
|
+
function babel(rootDir, overrides = {}) {
|
|
60
|
+
return {
|
|
61
|
+
presets: ['next/babel'],
|
|
62
|
+
plugins: [
|
|
63
|
+
[require.resolve('./babel.js'), stylexOptions(rootDir, overrides)],
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a complete postcss.config.js for Astryx source builds.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} rootDir — __dirname of the project root
|
|
72
|
+
* @param {object} [overrides] — extra options (appDir, extraInclude, etc.)
|
|
73
|
+
* @returns {object} postcss config object
|
|
74
|
+
*/
|
|
75
|
+
function postcss(rootDir, overrides = {}) {
|
|
76
|
+
const {appDir = 'src', extraInclude = [], ...rest} = overrides;
|
|
77
|
+
return {
|
|
78
|
+
plugins: {
|
|
79
|
+
[require.resolve('./index.js')]: {
|
|
80
|
+
cwd: rootDir,
|
|
81
|
+
appDir,
|
|
82
|
+
babelPlugins: [
|
|
83
|
+
['@stylexjs/babel-plugin', stylexOptions(rootDir, rest)],
|
|
84
|
+
],
|
|
85
|
+
extraInclude,
|
|
86
|
+
},
|
|
87
|
+
autoprefixer: {},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {babel, postcss, stylexOptions, resolveAliases};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @astryxdesign/postcss-plugin
|
|
7
|
+
*
|
|
8
|
+
* PostCSS plugin for Astryx source builds. Compiles StyleX from both
|
|
9
|
+
* Astryx library source and product code in two separate passes with
|
|
10
|
+
* different class name prefixes, then outputs them in separate layers:
|
|
11
|
+
*
|
|
12
|
+
* reset < astryx-base (library, prefix: 'astryx') < astryx-theme < product (prefix: 'x')
|
|
13
|
+
*
|
|
14
|
+
* The separate prefixes ensure atomic classes don't collide between
|
|
15
|
+
* layers, which would break theme overrides.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const postcss = require('postcss');
|
|
21
|
+
const babel = require('@babel/core');
|
|
22
|
+
const stylexBabelPlugin = require('@stylexjs/babel-plugin');
|
|
23
|
+
const {globSync} = require('fast-glob');
|
|
24
|
+
const isGlob = require('is-glob');
|
|
25
|
+
const globParent = require('glob-parent');
|
|
26
|
+
|
|
27
|
+
const PLUGIN_NAME = '@astryxdesign/postcss-plugin';
|
|
28
|
+
|
|
29
|
+
const LIBRARY_GLOB = 'node_modules/@astryxdesign/**/*.{ts,tsx}';
|
|
30
|
+
const LIBRARY_PATTERNS = ['node_modules/@astryxdesign/', 'packages/core/', 'packages/themes/'];
|
|
31
|
+
const STYLEX_IMPORT_SOURCE = '@stylexjs/stylex';
|
|
32
|
+
|
|
33
|
+
function parseDependency(fileOrGlob, cwd) {
|
|
34
|
+
if (fileOrGlob.startsWith('!')) return null;
|
|
35
|
+
if (isGlob(fileOrGlob)) {
|
|
36
|
+
const base = globParent(fileOrGlob);
|
|
37
|
+
let glob = fileOrGlob.substring(base === '.' ? 0 : base.length);
|
|
38
|
+
if (glob.charAt(0) === '/') glob = glob.substring(1);
|
|
39
|
+
return {
|
|
40
|
+
type: 'dir-dependency',
|
|
41
|
+
dir: path.normalize(path.resolve(cwd, base)),
|
|
42
|
+
glob,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: 'dependency',
|
|
47
|
+
file: path.normalize(path.resolve(cwd, fileOrGlob)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clone a babel plugins array, overriding the classNamePrefix on the
|
|
53
|
+
* StyleX babel plugin entry.
|
|
54
|
+
*/
|
|
55
|
+
function withClassNamePrefix(plugins, prefix) {
|
|
56
|
+
return plugins.map(p => {
|
|
57
|
+
if (!Array.isArray(p)) return p;
|
|
58
|
+
const [pluginPath, opts] = p;
|
|
59
|
+
// Match @stylexjs/babel-plugin by name
|
|
60
|
+
const name = typeof pluginPath === 'string' ? pluginPath : '';
|
|
61
|
+
if (name.includes('@stylexjs/babel-plugin') || name.includes('stylex')) {
|
|
62
|
+
return [pluginPath, {...opts, classNamePrefix: prefix}];
|
|
63
|
+
}
|
|
64
|
+
return p;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createPlugin() {
|
|
69
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
70
|
+
|
|
71
|
+
// Separate rule maps for library and product
|
|
72
|
+
const libraryRulesMap = new Map();
|
|
73
|
+
const productRulesMap = new Map();
|
|
74
|
+
const fileModifiedMap = new Map();
|
|
75
|
+
|
|
76
|
+
const plugin = ({
|
|
77
|
+
cwd = process.cwd(),
|
|
78
|
+
appDir = 'src',
|
|
79
|
+
babelPlugins = [],
|
|
80
|
+
layers = {
|
|
81
|
+
library: 'astryx-base',
|
|
82
|
+
product: 'product',
|
|
83
|
+
},
|
|
84
|
+
// Class name prefix for library styles (product keeps default 'x')
|
|
85
|
+
libraryPrefix = 'astryx',
|
|
86
|
+
extraInclude = [],
|
|
87
|
+
libraryPatterns = LIBRARY_PATTERNS,
|
|
88
|
+
exclude = [],
|
|
89
|
+
}) => {
|
|
90
|
+
const include = [
|
|
91
|
+
`${appDir}/**/*.{js,jsx,ts,tsx}`,
|
|
92
|
+
LIBRARY_GLOB,
|
|
93
|
+
...extraInclude,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const excludeWithDefaults = ['**/*.d.ts', '**/*.flow', ...exclude];
|
|
97
|
+
|
|
98
|
+
// Two babel configs — different classNamePrefix
|
|
99
|
+
const libraryBabelConfig = {
|
|
100
|
+
babelrc: false,
|
|
101
|
+
parserOpts: {plugins: ['typescript', 'jsx']},
|
|
102
|
+
plugins: withClassNamePrefix(babelPlugins, libraryPrefix),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const productBabelConfig = {
|
|
106
|
+
babelrc: false,
|
|
107
|
+
parserOpts: {plugins: ['typescript', 'jsx']},
|
|
108
|
+
plugins: babelPlugins, // keeps default 'x' prefix
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
let shouldSkipTransformError = false;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
postcssPlugin: PLUGIN_NAME,
|
|
115
|
+
plugins: [
|
|
116
|
+
async function (root, result) {
|
|
117
|
+
const fileName = result.opts.from;
|
|
118
|
+
|
|
119
|
+
let styleXAtRule = null;
|
|
120
|
+
root.walkAtRules((atRule) => {
|
|
121
|
+
if (atRule.name === 'stylex' && !atRule.params) {
|
|
122
|
+
styleXAtRule = atRule;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (styleXAtRule == null) return;
|
|
126
|
+
|
|
127
|
+
// Discover files
|
|
128
|
+
const files = new Set();
|
|
129
|
+
for (const pattern of include) {
|
|
130
|
+
const matched = globSync(pattern, {
|
|
131
|
+
onlyFiles: true,
|
|
132
|
+
ignore: excludeWithDefaults,
|
|
133
|
+
cwd,
|
|
134
|
+
});
|
|
135
|
+
for (const f of matched) {
|
|
136
|
+
files.add(path.normalize(path.resolve(cwd, f)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Watch dependencies
|
|
141
|
+
for (const pattern of include) {
|
|
142
|
+
const dep = parseDependency(pattern, cwd);
|
|
143
|
+
if (dep) {
|
|
144
|
+
result.messages.push({
|
|
145
|
+
plugin: PLUGIN_NAME,
|
|
146
|
+
parent: fileName,
|
|
147
|
+
...dep,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Remove deleted files
|
|
153
|
+
for (const f of fileModifiedMap.keys()) {
|
|
154
|
+
if (!files.has(f)) {
|
|
155
|
+
fileModifiedMap.delete(f);
|
|
156
|
+
libraryRulesMap.delete(f);
|
|
157
|
+
productRulesMap.delete(f);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Partition files into library vs product, then compile
|
|
162
|
+
// each group with its own babel config (different prefix)
|
|
163
|
+
const transforms = [];
|
|
164
|
+
for (const filePath of files) {
|
|
165
|
+
const mtimeMs = fs.existsSync(filePath)
|
|
166
|
+
? fs.statSync(filePath).mtimeMs
|
|
167
|
+
: -Infinity;
|
|
168
|
+
if (
|
|
169
|
+
fileModifiedMap.has(filePath) &&
|
|
170
|
+
mtimeMs === fileModifiedMap.get(filePath)
|
|
171
|
+
) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
fileModifiedMap.set(filePath, mtimeMs);
|
|
175
|
+
|
|
176
|
+
const contents = fs.readFileSync(filePath, 'utf-8');
|
|
177
|
+
if (!contents.includes(STYLEX_IMPORT_SOURCE)) continue;
|
|
178
|
+
|
|
179
|
+
const isLibrary = libraryPatterns.some(p => filePath.includes(p));
|
|
180
|
+
const config = isLibrary ? libraryBabelConfig : productBabelConfig;
|
|
181
|
+
const rulesMap = isLibrary ? libraryRulesMap : productRulesMap;
|
|
182
|
+
|
|
183
|
+
transforms.push(
|
|
184
|
+
babel
|
|
185
|
+
.transformAsync(contents, {
|
|
186
|
+
filename: filePath,
|
|
187
|
+
caller: {name: PLUGIN_NAME, platform: 'web', isDev},
|
|
188
|
+
...config,
|
|
189
|
+
})
|
|
190
|
+
.then(({metadata}) => {
|
|
191
|
+
const stylex = metadata?.stylex;
|
|
192
|
+
if (stylex != null && stylex.length > 0) {
|
|
193
|
+
rulesMap.set(filePath, stylex);
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.catch((error) => {
|
|
197
|
+
if (shouldSkipTransformError) {
|
|
198
|
+
console.warn(
|
|
199
|
+
`[${PLUGIN_NAME}] Failed to transform "${filePath}": ${error.message}`,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
await Promise.all(transforms);
|
|
208
|
+
|
|
209
|
+
// Collect rules from each map
|
|
210
|
+
const libraryRules = Array.from(libraryRulesMap.values()).flat();
|
|
211
|
+
const productRules = Array.from(productRulesMap.values()).flat();
|
|
212
|
+
|
|
213
|
+
// Process each group separately
|
|
214
|
+
const libraryCss = libraryRules.length
|
|
215
|
+
? stylexBabelPlugin.processStylexRules(libraryRules, {
|
|
216
|
+
useLayers: true,
|
|
217
|
+
})
|
|
218
|
+
: '';
|
|
219
|
+
const productCss = productRules.length
|
|
220
|
+
? stylexBabelPlugin.processStylexRules(productRules, {
|
|
221
|
+
useLayers: true,
|
|
222
|
+
})
|
|
223
|
+
: '';
|
|
224
|
+
|
|
225
|
+
// Wrap in named layers
|
|
226
|
+
const parts = [];
|
|
227
|
+
if (libraryCss) {
|
|
228
|
+
parts.push(`@layer ${layers.library} {\n${libraryCss}\n}`);
|
|
229
|
+
}
|
|
230
|
+
if (productCss) {
|
|
231
|
+
parts.push(`@layer ${layers.product} {\n${productCss}\n}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const finalCss = parts.join('\n\n');
|
|
235
|
+
const parsed = await postcss.parse(finalCss, {from: fileName});
|
|
236
|
+
styleXAtRule.replaceWith(parsed);
|
|
237
|
+
result.root = root;
|
|
238
|
+
|
|
239
|
+
if (!shouldSkipTransformError) {
|
|
240
|
+
shouldSkipTransformError = true;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
plugin.postcss = true;
|
|
248
|
+
return plugin;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = createPlugin();
|
package/src/next.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @astryxdesign/build/next
|
|
7
|
+
*
|
|
8
|
+
* Next.js configuration helper for Astryx source builds.
|
|
9
|
+
*
|
|
10
|
+
* Usage in next.config.mjs:
|
|
11
|
+
* import {withXDS} from '@astryxdesign/build/next';
|
|
12
|
+
* export default withXDS({
|
|
13
|
+
* // your normal next config
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a Next.js config to enable Astryx source builds.
|
|
19
|
+
* - Adds transpilePackages for @astryxdesign/* packages
|
|
20
|
+
* - Sets conditionNames to resolve source exports
|
|
21
|
+
*/
|
|
22
|
+
function withXDS(nextConfig = {}) {
|
|
23
|
+
const xdsPackages = [
|
|
24
|
+
'@astryxdesign/core',
|
|
25
|
+
'@astryxdesign/theme-default',
|
|
26
|
+
'@astryxdesign/theme-neutral',
|
|
27
|
+
'@astryxdesign/theme-brutalist',
|
|
28
|
+
'@astryxdesign/theme-daily',
|
|
29
|
+
'@astryxdesign/lab',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const existingTranspile = nextConfig.transpilePackages || [];
|
|
33
|
+
const merged = Array.from(new Set([...existingTranspile, ...xdsPackages]));
|
|
34
|
+
|
|
35
|
+
const existingWebpack = nextConfig.webpack;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...nextConfig,
|
|
39
|
+
transpilePackages: merged,
|
|
40
|
+
webpack: (config, context) => {
|
|
41
|
+
// Resolve to source exports
|
|
42
|
+
config.resolve.conditionNames = [
|
|
43
|
+
'source',
|
|
44
|
+
'import',
|
|
45
|
+
'require',
|
|
46
|
+
'default',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Preserve the symlinked node_modules path so Next.js's
|
|
50
|
+
// transpilePackages matcher recognizes @astryxdesign/* packages under
|
|
51
|
+
// pnpm's symlinked layout. Without this, webpack dereferences
|
|
52
|
+
// the symlink to packages/<name>/... which doesn't contain
|
|
53
|
+
// "node_modules/@astryxdesign" and transpilation is silently skipped,
|
|
54
|
+
// breaking subpath imports like '@astryxdesign/core/AlertDialog'.
|
|
55
|
+
config.resolve.symlinks = false;
|
|
56
|
+
|
|
57
|
+
// Call user's webpack config if provided
|
|
58
|
+
if (existingWebpack) {
|
|
59
|
+
return existingWebpack(config, context);
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {withXDS};
|
package/src/vite.test.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file vite.test.ts
|
|
5
|
+
* @description Verifies CSS layer-order injection in the XDS Vite plugin.
|
|
6
|
+
* The library layer name is configurable (default `astryx-base`); the
|
|
7
|
+
* theme layer name is fixed at `astryx-theme`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect} from 'vitest';
|
|
11
|
+
import {xdsStylex} from './vite';
|
|
12
|
+
|
|
13
|
+
/** Pull the injected `@layer ...;` order statement out of the plugin set. */
|
|
14
|
+
function getLayerOrder(plugins: ReturnType<typeof xdsStylex>): string {
|
|
15
|
+
const layerPlugin = plugins.find(p => p.name === 'xds-css-layer-order');
|
|
16
|
+
expect(layerPlugin, 'xds-css-layer-order plugin should exist').toBeTruthy();
|
|
17
|
+
const transform = (layerPlugin as any).transformIndexHtml;
|
|
18
|
+
const tags =
|
|
19
|
+
typeof transform === 'function' ? transform() : transform.handler();
|
|
20
|
+
const styleTag = tags.find((t: any) => t.tag === 'style');
|
|
21
|
+
expect(styleTag, 'a <style> tag should be injected').toBeTruthy();
|
|
22
|
+
return styleTag.children as string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('xdsStylex layer order (modern API)', () => {
|
|
26
|
+
it('uses the astryx-* layer names (theme layer is astryx-theme)', () => {
|
|
27
|
+
const order = getLayerOrder(xdsStylex());
|
|
28
|
+
expect(order).toBe('@layer reset, astryx-base, astryx-theme, product;');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('honors configured library and product layer names', () => {
|
|
32
|
+
const order = getLayerOrder(
|
|
33
|
+
xdsStylex({layers: {library: 'custom-base', product: 'app'}}),
|
|
34
|
+
);
|
|
35
|
+
// The theme layer stays astryx-theme regardless of other layer config.
|
|
36
|
+
expect(order).toBe('@layer reset, custom-base, astryx-theme, app;');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('xdsStylex layer order (legacy API)', () => {
|
|
41
|
+
it('uses the astryx-* layer names (theme layer is astryx-theme)', () => {
|
|
42
|
+
const order = getLayerOrder(xdsStylex({stylexOptions: {}}));
|
|
43
|
+
expect(order).toBe('@layer reset, astryx-base, astryx-theme, product;');
|
|
44
|
+
});
|
|
45
|
+
});
|