@yopem-ui/cli 0.0.2 → 0.1.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 +121 -3
- package/package.json +4 -2
- package/src/cli.ts +98 -18
- package/src/init.ts +370 -164
- package/src/install.ts +422 -59
- package/src/project.ts +223 -0
package/src/init.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type {
|
|
2
|
+
FileChange,
|
|
3
|
+
InstallOptions,
|
|
4
|
+
JsonObject,
|
|
5
|
+
JsonValue,
|
|
6
|
+
} from "@yopem-ui/cli/install"
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
existingFile,
|
|
9
10
|
installItem,
|
|
10
11
|
isRecord,
|
|
11
12
|
isString,
|
|
12
|
-
|
|
13
|
-
} from "
|
|
13
|
+
writeFiles,
|
|
14
|
+
} from "@yopem-ui/cli/install"
|
|
15
|
+
import {
|
|
16
|
+
isPackageName,
|
|
17
|
+
packageRunner,
|
|
18
|
+
workspaceRoot,
|
|
19
|
+
} from "@yopem-ui/cli/project"
|
|
20
|
+
import { readFile, realpath } from "node:fs/promises"
|
|
21
|
+
import { join, relative, resolve } from "node:path"
|
|
22
|
+
import ts from "typescript-api"
|
|
14
23
|
|
|
15
24
|
type Framework =
|
|
16
25
|
| "vite"
|
|
@@ -20,7 +29,10 @@ type Framework =
|
|
|
20
29
|
| "next"
|
|
21
30
|
| "astro"
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
interface SharedUI {
|
|
33
|
+
name: string
|
|
34
|
+
source: string
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
interface Edit {
|
|
26
38
|
start: number
|
|
@@ -30,6 +42,7 @@ interface Edit {
|
|
|
30
42
|
|
|
31
43
|
export interface InitOptions extends InstallOptions {
|
|
32
44
|
framework?: Framework
|
|
45
|
+
ui?: string
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
function object(value: JsonValue | undefined): value is JsonObject {
|
|
@@ -266,6 +279,19 @@ function addAlias(
|
|
|
266
279
|
) {
|
|
267
280
|
throw new Error(`Incompatible @ alias in ${path}`)
|
|
268
281
|
}
|
|
282
|
+
|
|
283
|
+
const aliasValue = declaration?.initializer ?? current.initializer
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
ts.isStringLiteral(aliasValue) &&
|
|
287
|
+
/^(?:\.\/)?src\/?$/.test(aliasValue.text)
|
|
288
|
+
) {
|
|
289
|
+
edits.push({
|
|
290
|
+
start: current.initializer.getStart(source),
|
|
291
|
+
end: current.initializer.end,
|
|
292
|
+
text: "yopemSource",
|
|
293
|
+
})
|
|
294
|
+
}
|
|
269
295
|
}
|
|
270
296
|
|
|
271
297
|
function moduleConfig(source: ts.SourceFile, path: string) {
|
|
@@ -549,14 +575,18 @@ function babelPlugin(presets = false) {
|
|
|
549
575
|
return `babel({ plugins: yopemBabelPlugins${presets ? ", presets: yopemBabelPresets" : ""} })`
|
|
550
576
|
}
|
|
551
577
|
|
|
552
|
-
function stylexSetup(
|
|
578
|
+
function stylexSetup(
|
|
579
|
+
babelPlugins: string[] = [],
|
|
580
|
+
babelPresets: string[] = [],
|
|
581
|
+
shared?: SharedUI,
|
|
582
|
+
) {
|
|
553
583
|
return `import babel from "@rolldown/plugin-babel"
|
|
554
584
|
import { createRequire as yopemCreateRequire } from "node:module"
|
|
555
585
|
import { fileURLToPath as yopemFileURLToPath } from "node:url"
|
|
556
586
|
|
|
557
|
-
const yopemSource = yopemFileURLToPath(new URL("./src", import.meta.url))
|
|
587
|
+
const yopemSource = yopemFileURLToPath(new URL("./src", import.meta.url))${shared ? `\nconst yopemUISource = yopemFileURLToPath(new URL(${JSON.stringify(shared.source)}, import.meta.url))` : ""}
|
|
558
588
|
const yopemBabelPlugins = [${babelPlugins.join(", ")}${babelPlugins.length ? ", " : ""}["@stylexjs/babel-plugin", {
|
|
559
|
-
aliases: { "@/*": [yopemSource + "/*"] },
|
|
589
|
+
aliases: { "@/*": [yopemSource + "/*"]${shared ? `, ${JSON.stringify(`${shared.name}/*`)}: [yopemUISource + "/*"]` : ""} },
|
|
560
590
|
dev: process.env.NODE_ENV !== "production",
|
|
561
591
|
runtimeInjection: false,
|
|
562
592
|
treeshakeCompensation: true,
|
|
@@ -564,7 +594,7 @@ const yopemBabelPlugins = [${babelPlugins.join(", ")}${babelPlugins.length ? ",
|
|
|
564
594
|
}]]${babelPresets.length ? `\nconst yopemBabelPresets = [${babelPresets.join(", ")}]` : ""}
|
|
565
595
|
const yopemPostcssPlugin = yopemCreateRequire(import.meta.url)("@stylexjs/postcss-plugin")({
|
|
566
596
|
cwd: yopemFileURLToPath(new URL(".", import.meta.url)),
|
|
567
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}"],
|
|
597
|
+
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}"${shared ? ', yopemUISource + "/**/*.{js,jsx,ts,tsx}"' : ""}],
|
|
568
598
|
babelConfig: {
|
|
569
599
|
babelrc: false,
|
|
570
600
|
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
@@ -697,10 +727,20 @@ function viteConfig(
|
|
|
697
727
|
path: string,
|
|
698
728
|
babelConfig: ReturnType<typeof migrateBabel>,
|
|
699
729
|
postcssPlugins: string[],
|
|
730
|
+
shared?: SharedUI,
|
|
700
731
|
) {
|
|
701
732
|
content = stripLegacyConfig(content, path)
|
|
702
733
|
|
|
703
734
|
if (content.includes("stylex.vite(") || hasYopemStylexConfig(content)) {
|
|
735
|
+
if (
|
|
736
|
+
shared &&
|
|
737
|
+
(!content.includes(JSON.stringify(`${shared.name}/*`)) ||
|
|
738
|
+
!content.includes(JSON.stringify(shared.source)))
|
|
739
|
+
)
|
|
740
|
+
throw new Error(
|
|
741
|
+
`Existing StyleX configuration targets a different UI package in ${path}`,
|
|
742
|
+
)
|
|
743
|
+
|
|
704
744
|
if (!content.includes('"@":') && !content.includes("'@':")) {
|
|
705
745
|
throw new Error(`Existing StyleX configuration lacks @ alias in ${path}`)
|
|
706
746
|
}
|
|
@@ -736,7 +776,7 @@ function viteConfig(
|
|
|
736
776
|
edits.push({
|
|
737
777
|
start: 0,
|
|
738
778
|
end: 0,
|
|
739
|
-
text: stylexSetup(babelConfig.plugins, babelConfig.presets),
|
|
779
|
+
text: stylexSetup(babelConfig.plugins, babelConfig.presets, shared),
|
|
740
780
|
})
|
|
741
781
|
|
|
742
782
|
return applyEdits(content, edits)
|
|
@@ -747,6 +787,7 @@ function astroConfig(
|
|
|
747
787
|
path: string,
|
|
748
788
|
babelConfig: ReturnType<typeof migrateBabel>,
|
|
749
789
|
postcssPlugins: string[],
|
|
790
|
+
shared?: SharedUI,
|
|
750
791
|
) {
|
|
751
792
|
content = stripLegacyConfig(content, path)
|
|
752
793
|
const source = parsed(path, content)
|
|
@@ -772,6 +813,15 @@ function astroConfig(
|
|
|
772
813
|
hasYopemStylexConfig(content) && content.includes(`${reactName}()`)
|
|
773
814
|
|
|
774
815
|
if (alreadyConfigured) {
|
|
816
|
+
if (
|
|
817
|
+
shared &&
|
|
818
|
+
(!content.includes(JSON.stringify(`${shared.name}/*`)) ||
|
|
819
|
+
!content.includes(JSON.stringify(shared.source)))
|
|
820
|
+
)
|
|
821
|
+
throw new Error(
|
|
822
|
+
`Existing StyleX configuration targets a different UI package in ${path}`,
|
|
823
|
+
)
|
|
824
|
+
|
|
775
825
|
if (!content.includes('"@":') && !content.includes("'@':")) {
|
|
776
826
|
throw new Error(`Existing StyleX configuration lacks @ alias in ${path}`)
|
|
777
827
|
}
|
|
@@ -847,7 +897,7 @@ function astroConfig(
|
|
|
847
897
|
edits.push({
|
|
848
898
|
start: 0,
|
|
849
899
|
end: 0,
|
|
850
|
-
text: `${reactImport ? "" : 'import react from "@astrojs/react"\n'}${stylexSetup(babelConfig.plugins, babelConfig.presets)}`,
|
|
900
|
+
text: `${reactImport ? "" : 'import react from "@astrojs/react"\n'}${stylexSetup(babelConfig.plugins, babelConfig.presets, shared)}`,
|
|
851
901
|
})
|
|
852
902
|
|
|
853
903
|
return applyEdits(content, edits)
|
|
@@ -924,10 +974,10 @@ function tsconfig(content: string, path: string) {
|
|
|
924
974
|
return applyEdits(content, edits)
|
|
925
975
|
}
|
|
926
976
|
|
|
927
|
-
function reactEntry(content: string, path: string) {
|
|
977
|
+
function reactEntry(content: string, path: string, prefix = "@") {
|
|
928
978
|
if (content.includes("themeMarker") || content.includes("rootStyles.html")) {
|
|
929
979
|
if (
|
|
930
|
-
!content.includes(
|
|
980
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
931
981
|
!content.includes("rootStyles.html") ||
|
|
932
982
|
!content.includes('dataset.theme = "light"')
|
|
933
983
|
) {
|
|
@@ -947,7 +997,7 @@ function reactEntry(content: string, path: string) {
|
|
|
947
997
|
{
|
|
948
998
|
start: 0,
|
|
949
999
|
end: 0,
|
|
950
|
-
text:
|
|
1000
|
+
text: `import "${prefix}/styles/styles.css"\nimport * as stylex from "@stylexjs/stylex"\nimport { lightTheme, rootStyles, themeMarker } from "${prefix}/styles/tokens.stylex"\n`,
|
|
951
1001
|
},
|
|
952
1002
|
{
|
|
953
1003
|
start: last.end,
|
|
@@ -961,10 +1011,11 @@ function jsxLayout(
|
|
|
961
1011
|
content: string,
|
|
962
1012
|
path: string,
|
|
963
1013
|
framework: "next" | "tanstack-start" | "react-router",
|
|
1014
|
+
prefix = "@",
|
|
964
1015
|
) {
|
|
965
1016
|
if (content.includes("stylexProps(") || content.includes("rootStyles.html")) {
|
|
966
1017
|
if (
|
|
967
|
-
!content.includes(
|
|
1018
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
968
1019
|
!content.includes("rootStyles.html") ||
|
|
969
1020
|
!content.includes("rootStyles.body")
|
|
970
1021
|
) {
|
|
@@ -1064,20 +1115,20 @@ function jsxLayout(
|
|
|
1064
1115
|
})
|
|
1065
1116
|
}
|
|
1066
1117
|
|
|
1067
|
-
const css =
|
|
1118
|
+
const css = `import "${prefix}/styles/styles.css"\n`
|
|
1068
1119
|
edits.push({
|
|
1069
1120
|
start: 0,
|
|
1070
1121
|
end: 0,
|
|
1071
|
-
text: `${css}import { stylexProps } from "
|
|
1122
|
+
text: `${css}import { stylexProps } from "${prefix}/lib/stylex"\nimport { lightTheme, rootStyles, themeMarker } from "${prefix}/styles/tokens.stylex"\n`,
|
|
1072
1123
|
})
|
|
1073
1124
|
|
|
1074
1125
|
return applyEdits(content, edits)
|
|
1075
1126
|
}
|
|
1076
1127
|
|
|
1077
|
-
function astroLayout(content: string, path: string) {
|
|
1128
|
+
function astroLayout(content: string, path: string, prefix = "@") {
|
|
1078
1129
|
if (content.includes("yopemHtml.className")) {
|
|
1079
1130
|
if (
|
|
1080
|
-
!content.includes(
|
|
1131
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
1081
1132
|
!content.includes("yopemBody.className")
|
|
1082
1133
|
) {
|
|
1083
1134
|
throw new Error(`Incomplete Yopem layout in ${path}`)
|
|
@@ -1117,7 +1168,7 @@ function astroLayout(content: string, path: string) {
|
|
|
1117
1168
|
{
|
|
1118
1169
|
start: end,
|
|
1119
1170
|
end,
|
|
1120
|
-
text: `${content.includes(
|
|
1171
|
+
text: `${content.includes(`"${prefix}/styles/styles.css"`) ? "" : `\nimport "${prefix}/styles/styles.css"`}\nimport * as stylex from "@stylexjs/stylex"\nimport { lightTheme, rootStyles, themeMarker } from "${prefix}/styles/tokens.stylex"\n\nconst yopemHtml = stylex.props(themeMarker, lightTheme, rootStyles.html)\nconst yopemBody = stylex.props(rootStyles.body)`,
|
|
1121
1172
|
},
|
|
1122
1173
|
]
|
|
1123
1174
|
|
|
@@ -1170,7 +1221,7 @@ function astroLayout(content: string, path: string) {
|
|
|
1170
1221
|
return applyEdits(content, edits)
|
|
1171
1222
|
}
|
|
1172
1223
|
|
|
1173
|
-
function nextBabel(esm: boolean) {
|
|
1224
|
+
function nextBabel(esm: boolean, shared?: SharedUI) {
|
|
1174
1225
|
const header = esm
|
|
1175
1226
|
? `import { createRequire } from "node:module"
|
|
1176
1227
|
import { dirname } from "node:path"
|
|
@@ -1208,7 +1259,7 @@ function expandLocalSpreads({ types: t }) {
|
|
|
1208
1259
|
}
|
|
1209
1260
|
|
|
1210
1261
|
${esm ? 'export const presets = ["next/babel"]\nexport const plugins = [' : 'module.exports = {\n presets: ["next/babel"],\n plugins: ['}expandLocalSpreads, ["@stylexjs/babel-plugin", {
|
|
1211
|
-
aliases: { "@/*": [path.join(__dirname, "src/*")] },
|
|
1262
|
+
aliases: { "@/*": [path.join(__dirname, "src/*")]${shared ? `, ${JSON.stringify(`${shared.name}/*`)}: [path.join(__dirname, ${JSON.stringify(`${shared.source}*`)})]` : ""} },
|
|
1212
1263
|
dev: process.env.NODE_ENV !== "production",
|
|
1213
1264
|
runtimeInjection: false,
|
|
1214
1265
|
treeshakeCompensation: true,
|
|
@@ -1217,7 +1268,40 @@ ${esm ? 'export const presets = ["next/babel"]\nexport const plugins = [' : 'mod
|
|
|
1217
1268
|
`
|
|
1218
1269
|
}
|
|
1219
1270
|
|
|
1220
|
-
function
|
|
1271
|
+
function nextConfig(content: string, path: string, name: string) {
|
|
1272
|
+
const source = parsed(path, content)
|
|
1273
|
+
const config = moduleConfig(source, path)
|
|
1274
|
+
const packages = property(config, "transpilePackages")
|
|
1275
|
+
const edits: Edit[] = []
|
|
1276
|
+
|
|
1277
|
+
if (!packages) {
|
|
1278
|
+
addProperty(
|
|
1279
|
+
source,
|
|
1280
|
+
config,
|
|
1281
|
+
`transpilePackages: [${JSON.stringify(name)}]`,
|
|
1282
|
+
edits,
|
|
1283
|
+
)
|
|
1284
|
+
} else {
|
|
1285
|
+
if (!ts.isArrayLiteralExpression(packages.initializer))
|
|
1286
|
+
throw new Error(`Unsupported transpilePackages in ${path}`)
|
|
1287
|
+
|
|
1288
|
+
if (
|
|
1289
|
+
!packages.initializer.elements.some(
|
|
1290
|
+
(entry) => ts.isStringLiteral(entry) && entry.text === name,
|
|
1291
|
+
)
|
|
1292
|
+
)
|
|
1293
|
+
addArrayEntries(
|
|
1294
|
+
source,
|
|
1295
|
+
packages.initializer,
|
|
1296
|
+
[JSON.stringify(name)],
|
|
1297
|
+
edits,
|
|
1298
|
+
)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return applyEdits(content, edits)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function nextPostcss(shared?: SharedUI) {
|
|
1221
1305
|
return `// Next loads this PostCSS config synchronously through CommonJS.
|
|
1222
1306
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1223
1307
|
const babelConfig = require("./babel.config.js")
|
|
@@ -1225,7 +1309,7 @@ const babelConfig = require("./babel.config.js")
|
|
|
1225
1309
|
module.exports = {
|
|
1226
1310
|
plugins: {
|
|
1227
1311
|
"@stylexjs/postcss-plugin": {
|
|
1228
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"],
|
|
1312
|
+
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"${shared ? `, require("node:path").join(__dirname, ${JSON.stringify(`${shared.source}**/*.{js,jsx,ts,tsx}`)})` : ""}],
|
|
1229
1313
|
babelConfig: {
|
|
1230
1314
|
babelrc: false,
|
|
1231
1315
|
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
@@ -1239,7 +1323,12 @@ module.exports = {
|
|
|
1239
1323
|
`
|
|
1240
1324
|
}
|
|
1241
1325
|
|
|
1242
|
-
function postcss(
|
|
1326
|
+
function postcss(
|
|
1327
|
+
content: string,
|
|
1328
|
+
path: string,
|
|
1329
|
+
esm: boolean,
|
|
1330
|
+
shared?: SharedUI,
|
|
1331
|
+
) {
|
|
1243
1332
|
if (content.includes('"@stylexjs/postcss-plugin"')) return content
|
|
1244
1333
|
const source = parsed(path, content)
|
|
1245
1334
|
let config: ts.ObjectLiteralExpression | undefined
|
|
@@ -1269,7 +1358,7 @@ function postcss(content: string, path: string, esm: boolean) {
|
|
|
1269
1358
|
source,
|
|
1270
1359
|
plugins,
|
|
1271
1360
|
`"@stylexjs/postcss-plugin": {
|
|
1272
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"],
|
|
1361
|
+
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"${shared ? `, ${esm ? "yopemRequire" : "require"}("node:path").resolve(${esm ? 'yopemRequire("node:url").fileURLToPath(new URL(".", import.meta.url))' : "__dirname"}, ${JSON.stringify(`${shared.source}**/*.{js,jsx,ts,tsx}`)})` : ""}],
|
|
1273
1362
|
babelConfig: { babelrc: false, parserOpts: { plugins: ["typescript", "jsx"] }, plugins: yopemBabelConfig.plugins },
|
|
1274
1363
|
useCSSLayers: true,
|
|
1275
1364
|
}`,
|
|
@@ -1279,7 +1368,7 @@ function postcss(content: string, path: string, esm: boolean) {
|
|
|
1279
1368
|
start: 0,
|
|
1280
1369
|
end: 0,
|
|
1281
1370
|
text: esm
|
|
1282
|
-
? 'import { createRequire } from "node:module"\nconst
|
|
1371
|
+
? 'import { createRequire } from "node:module"\nconst yopemRequire = createRequire(import.meta.url)\nconst yopemBabelConfig = yopemRequire("./babel.config.js")\n'
|
|
1283
1372
|
: 'const yopemBabelConfig = require("./babel.config.js")\n',
|
|
1284
1373
|
})
|
|
1285
1374
|
|
|
@@ -1295,7 +1384,7 @@ const lintRules = {
|
|
|
1295
1384
|
"yopem-ui/valid-polymorphic-as": "error",
|
|
1296
1385
|
}
|
|
1297
1386
|
|
|
1298
|
-
function lintConfig(content: string) {
|
|
1387
|
+
function lintConfig(content: string, shared?: SharedUI) {
|
|
1299
1388
|
const config: JsonValue = JSON.parse(content)
|
|
1300
1389
|
|
|
1301
1390
|
if (!object(config)) throw new Error("Invalid .oxlintrc.json")
|
|
@@ -1325,6 +1414,45 @@ function lintConfig(content: string) {
|
|
|
1325
1414
|
throw new Error("Conflicting yopem-ui plugin in .oxlintrc.json")
|
|
1326
1415
|
}
|
|
1327
1416
|
|
|
1417
|
+
const configuredRules: JsonObject = { ...lintRules, ...rules }
|
|
1418
|
+
|
|
1419
|
+
if (shared) {
|
|
1420
|
+
for (const name of [
|
|
1421
|
+
"yopem-ui/enforce-styling-methods",
|
|
1422
|
+
"yopem-ui/no-restyle",
|
|
1423
|
+
"yopem-ui/valid-polymorphic-as",
|
|
1424
|
+
]) {
|
|
1425
|
+
const setting = configuredRules[name] ?? "error"
|
|
1426
|
+
const level = Array.isArray(setting) ? (setting[0] ?? "error") : setting
|
|
1427
|
+
|
|
1428
|
+
if (level === "off" || level === 0) continue
|
|
1429
|
+
|
|
1430
|
+
const options = Array.isArray(setting) ? (setting[1] ?? {}) : {}
|
|
1431
|
+
|
|
1432
|
+
if (!object(options)) throw new Error(`Invalid options for ${name}`)
|
|
1433
|
+
|
|
1434
|
+
const sources = options.componentSources ?? [
|
|
1435
|
+
"@/components/ui/",
|
|
1436
|
+
"@registry/components/ui/",
|
|
1437
|
+
"@yopem-ui/ui",
|
|
1438
|
+
]
|
|
1439
|
+
|
|
1440
|
+
if (!Array.isArray(sources) || !sources.every(isString)) {
|
|
1441
|
+
throw new Error(`Invalid componentSources for ${name}`)
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
configuredRules[name] = [
|
|
1445
|
+
level,
|
|
1446
|
+
{
|
|
1447
|
+
...options,
|
|
1448
|
+
componentSources: [
|
|
1449
|
+
...new Set([...sources, `${shared.name}/components/ui/`]),
|
|
1450
|
+
],
|
|
1451
|
+
},
|
|
1452
|
+
]
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1328
1456
|
const next = {
|
|
1329
1457
|
...config,
|
|
1330
1458
|
jsPlugins: existing
|
|
@@ -1333,7 +1461,7 @@ function lintConfig(content: string) {
|
|
|
1333
1461
|
...plugins,
|
|
1334
1462
|
{ name: "yopem-ui", specifier: "@yopem-ui/oxlint-plugin" },
|
|
1335
1463
|
],
|
|
1336
|
-
rules:
|
|
1464
|
+
rules: configuredRules,
|
|
1337
1465
|
overrides: overrides.some(
|
|
1338
1466
|
(entry) => JSON.stringify(entry) === JSON.stringify(componentOverride),
|
|
1339
1467
|
)
|
|
@@ -1419,48 +1547,8 @@ function detectFramework(
|
|
|
1419
1547
|
return detected[0]!
|
|
1420
1548
|
}
|
|
1421
1549
|
|
|
1422
|
-
async function packageManager(
|
|
1423
|
-
root: string,
|
|
1424
|
-
declared: JsonValue | undefined,
|
|
1425
|
-
): Promise<PackageManager> {
|
|
1426
|
-
const markers: [PackageManager, string][] = [
|
|
1427
|
-
["bun", "bun.lock"],
|
|
1428
|
-
["bun", "bun.lockb"],
|
|
1429
|
-
["npm", "package-lock.json"],
|
|
1430
|
-
["pnpm", "pnpm-lock.yaml"],
|
|
1431
|
-
["yarn", "yarn.lock"],
|
|
1432
|
-
]
|
|
1433
|
-
|
|
1434
|
-
const found = new Set<PackageManager>()
|
|
1435
|
-
|
|
1436
|
-
for (const [manager, path] of markers) {
|
|
1437
|
-
if (await existingFile(root, path)) found.add(manager)
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (found.size > 1) throw new Error("Conflicting package manager lockfiles")
|
|
1441
|
-
|
|
1442
|
-
const named = isString(declared) ? declared.split("@")[0] : undefined
|
|
1443
|
-
|
|
1444
|
-
if (
|
|
1445
|
-
named &&
|
|
1446
|
-
named !== "bun" &&
|
|
1447
|
-
named !== "npm" &&
|
|
1448
|
-
named !== "pnpm" &&
|
|
1449
|
-
named !== "yarn"
|
|
1450
|
-
) {
|
|
1451
|
-
throw new Error(`Unsupported package manager: ${named}`)
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
const locked = [...found][0]
|
|
1455
|
-
|
|
1456
|
-
if (locked && named && locked !== named) {
|
|
1457
|
-
throw new Error("Package manager and lockfile disagree")
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
return locked ?? named ?? "bun"
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
1550
|
export async function initProject(options: InitOptions = {}) {
|
|
1551
|
+
if (options.dryRun) throw new Error("--dry-run is not supported for init")
|
|
1464
1552
|
const root = await realpath(options.cwd ?? process.cwd())
|
|
1465
1553
|
|
|
1466
1554
|
if (!(await existingFile(root, "package.json"))) {
|
|
@@ -1484,7 +1572,33 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1484
1572
|
)
|
|
1485
1573
|
|
|
1486
1574
|
const framework = detectFramework(dependencies, options.framework)
|
|
1487
|
-
const
|
|
1575
|
+
const uiRoot = options.ui ? await realpath(resolve(root, options.ui)) : root
|
|
1576
|
+
let shared: SharedUI | undefined
|
|
1577
|
+
|
|
1578
|
+
if (options.ui) {
|
|
1579
|
+
if (uiRoot === root)
|
|
1580
|
+
throw new Error("Shared UI package must differ from app")
|
|
1581
|
+
|
|
1582
|
+
const uiManifest: JsonValue = JSON.parse(
|
|
1583
|
+
await readFile(join(uiRoot, "package.json"), "utf8"),
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
if (!object(uiManifest) || !isPackageName(uiManifest.name))
|
|
1587
|
+
throw new Error("Shared UI package requires a valid package name")
|
|
1588
|
+
|
|
1589
|
+
const owner = await workspaceRoot(root)
|
|
1590
|
+
|
|
1591
|
+
if ((await workspaceRoot(uiRoot)) !== owner)
|
|
1592
|
+
throw new Error("App and UI package must belong to the same workspace")
|
|
1593
|
+
|
|
1594
|
+
shared = {
|
|
1595
|
+
name: uiManifest.name,
|
|
1596
|
+
source: `${relative(root, join(uiRoot, "src")).replaceAll("\\", "/")}/`,
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const packageRun = await packageRunner(root, options.run)
|
|
1601
|
+
const uiRun = await packageRunner(uiRoot, options.run)
|
|
1488
1602
|
const edits = new Map<string, { before: string | null; after: string }>()
|
|
1489
1603
|
|
|
1490
1604
|
async function plan(
|
|
@@ -1550,7 +1664,49 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1550
1664
|
: "tsconfig.json"
|
|
1551
1665
|
|
|
1552
1666
|
await plan(tsPath, (content) => tsconfig(content, tsPath))
|
|
1553
|
-
await plan(".oxlintrc.json", lintConfig, "{}")
|
|
1667
|
+
await plan(".oxlintrc.json", (content) => lintConfig(content, shared), "{}")
|
|
1668
|
+
|
|
1669
|
+
if (shared) {
|
|
1670
|
+
const prefix = relative(root, uiRoot).replaceAll("\\", "/")
|
|
1671
|
+
|
|
1672
|
+
await plan(`${prefix}/package.json`, (content) => {
|
|
1673
|
+
const value: JsonValue = JSON.parse(content)
|
|
1674
|
+
|
|
1675
|
+
if (
|
|
1676
|
+
!object(value) ||
|
|
1677
|
+
(value.exports !== undefined && !object(value.exports))
|
|
1678
|
+
)
|
|
1679
|
+
throw new Error("Shared UI package requires object exports")
|
|
1680
|
+
const exports = object(value.exports) ? { ...value.exports } : {}
|
|
1681
|
+
|
|
1682
|
+
const paths = {
|
|
1683
|
+
"./components/ui/*": "./src/components/ui/*.tsx",
|
|
1684
|
+
"./lib/*": "./src/lib/*.ts",
|
|
1685
|
+
"./styles/tokens.stylex": "./src/styles/tokens.stylex.ts",
|
|
1686
|
+
"./styles/styles.css": "./src/styles/styles.css",
|
|
1687
|
+
"./theme/*": "./src/theme/*.tsx",
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
for (const [path, target] of Object.entries(paths)) {
|
|
1691
|
+
if (exports[path] !== undefined && exports[path] !== target)
|
|
1692
|
+
throw new Error(`Conflicting UI package export: ${path}`)
|
|
1693
|
+
exports[path] = target
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const sideEffects =
|
|
1697
|
+
value.sideEffects === false
|
|
1698
|
+
? ["**/*.css"]
|
|
1699
|
+
: Array.isArray(value.sideEffects) &&
|
|
1700
|
+
!value.sideEffects.includes("**/*.css")
|
|
1701
|
+
? [...value.sideEffects, "**/*.css"]
|
|
1702
|
+
: value.sideEffects
|
|
1703
|
+
|
|
1704
|
+
return JSON.stringify(value.exports) === JSON.stringify(exports) &&
|
|
1705
|
+
JSON.stringify(value.sideEffects) === JSON.stringify(sideEffects)
|
|
1706
|
+
? content
|
|
1707
|
+
: `${JSON.stringify({ ...value, exports, sideEffects }, null, 2)}\n`
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1554
1710
|
|
|
1555
1711
|
const devDependencies = [
|
|
1556
1712
|
"oxlint@^1.79.0",
|
|
@@ -1561,7 +1717,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1561
1717
|
"@stylexjs/postcss-plugin@^0.19.0",
|
|
1562
1718
|
]
|
|
1563
1719
|
|
|
1564
|
-
const runtimeDependencies: string[] =
|
|
1720
|
+
const runtimeDependencies: string[] = shared
|
|
1721
|
+
? [`${shared.name}@workspace:*`, "@stylexjs/stylex@^0.19.0"]
|
|
1722
|
+
: []
|
|
1565
1723
|
|
|
1566
1724
|
if (framework === "astro") {
|
|
1567
1725
|
const config = await chooseFile(root, [
|
|
@@ -1571,7 +1729,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1571
1729
|
])
|
|
1572
1730
|
|
|
1573
1731
|
await plan(config, (content) =>
|
|
1574
|
-
astroConfig(content, config, babelConfig, postcssPlugins),
|
|
1732
|
+
astroConfig(content, config, babelConfig, postcssPlugins, shared),
|
|
1575
1733
|
)
|
|
1576
1734
|
|
|
1577
1735
|
const layout = await chooseFile(root, [
|
|
@@ -1579,12 +1737,27 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1579
1737
|
"src/layouts/layout.astro",
|
|
1580
1738
|
])
|
|
1581
1739
|
|
|
1582
|
-
await plan(layout, (content) => astroLayout(content, layout))
|
|
1740
|
+
await plan(layout, (content) => astroLayout(content, layout, shared?.name))
|
|
1583
1741
|
|
|
1584
1742
|
for (const name of ["@astrojs/react", "react", "react-dom"]) {
|
|
1585
1743
|
if (!(name in dependencies)) runtimeDependencies.push(name)
|
|
1586
1744
|
}
|
|
1587
1745
|
} else if (framework === "next") {
|
|
1746
|
+
if (shared) {
|
|
1747
|
+
const config = await chooseFile(
|
|
1748
|
+
root,
|
|
1749
|
+
["next.config.ts", "next.config.mjs", "next.config.js"],
|
|
1750
|
+
"next.config.mjs",
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1753
|
+
const name = shared.name
|
|
1754
|
+
await plan(
|
|
1755
|
+
config,
|
|
1756
|
+
(content) => nextConfig(content, config, name),
|
|
1757
|
+
"export default {}\n",
|
|
1758
|
+
)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1588
1761
|
if (!object(manifest.scripts))
|
|
1589
1762
|
throw new Error("Next.js scripts are missing")
|
|
1590
1763
|
nextScripts(manifest.scripts)
|
|
@@ -1594,7 +1767,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1594
1767
|
"app/layout.tsx",
|
|
1595
1768
|
])
|
|
1596
1769
|
|
|
1597
|
-
await plan(layout, (content) =>
|
|
1770
|
+
await plan(layout, (content) =>
|
|
1771
|
+
jsxLayout(content, layout, "next", shared?.name),
|
|
1772
|
+
)
|
|
1598
1773
|
|
|
1599
1774
|
const babel = await chooseFile(
|
|
1600
1775
|
root,
|
|
@@ -1612,7 +1787,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1612
1787
|
throw new Error(`Unsupported Babel configuration: ${babel}`)
|
|
1613
1788
|
}
|
|
1614
1789
|
|
|
1615
|
-
const babelContent = nextBabel(manifest.type === "module")
|
|
1790
|
+
const babelContent = nextBabel(manifest.type === "module", shared)
|
|
1616
1791
|
await plan(
|
|
1617
1792
|
babel,
|
|
1618
1793
|
(content) => {
|
|
@@ -1634,15 +1809,16 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1634
1809
|
await plan(
|
|
1635
1810
|
postcssPath,
|
|
1636
1811
|
(content) =>
|
|
1637
|
-
postcssPath === "postcss.config.cjs" && content === nextPostcss()
|
|
1812
|
+
postcssPath === "postcss.config.cjs" && content === nextPostcss(shared)
|
|
1638
1813
|
? content
|
|
1639
1814
|
: postcss(
|
|
1640
1815
|
content,
|
|
1641
1816
|
postcssPath,
|
|
1642
1817
|
postcssPath.endsWith(".mjs") ||
|
|
1643
1818
|
(postcssPath.endsWith(".js") && manifest.type === "module"),
|
|
1819
|
+
shared,
|
|
1644
1820
|
),
|
|
1645
|
-
nextPostcss(),
|
|
1821
|
+
nextPostcss(shared),
|
|
1646
1822
|
)
|
|
1647
1823
|
devDependencies.splice(
|
|
1648
1824
|
0,
|
|
@@ -1663,7 +1839,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1663
1839
|
])
|
|
1664
1840
|
|
|
1665
1841
|
await plan(config, (content) =>
|
|
1666
|
-
viteConfig(content, config, babelConfig, postcssPlugins),
|
|
1842
|
+
viteConfig(content, config, babelConfig, postcssPlugins, shared),
|
|
1667
1843
|
)
|
|
1668
1844
|
|
|
1669
1845
|
if (
|
|
@@ -1677,7 +1853,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1677
1853
|
: ["app/root.tsx", "src/root.tsx"],
|
|
1678
1854
|
)
|
|
1679
1855
|
|
|
1680
|
-
await plan(layout, (content) =>
|
|
1856
|
+
await plan(layout, (content) =>
|
|
1857
|
+
jsxLayout(content, layout, framework, shared?.name),
|
|
1858
|
+
)
|
|
1681
1859
|
} else {
|
|
1682
1860
|
const entry = await chooseFile(root, [
|
|
1683
1861
|
"src/main.tsx",
|
|
@@ -1686,110 +1864,138 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1686
1864
|
"src/index.jsx",
|
|
1687
1865
|
])
|
|
1688
1866
|
|
|
1689
|
-
await plan(entry, (content) => reactEntry(content, entry))
|
|
1867
|
+
await plan(entry, (content) => reactEntry(content, entry, shared?.name))
|
|
1690
1868
|
}
|
|
1691
1869
|
}
|
|
1692
1870
|
|
|
1693
|
-
|
|
1694
|
-
options.run ??
|
|
1695
|
-
(manager === "bun"
|
|
1696
|
-
? runBun
|
|
1697
|
-
: async function runPackage(args: string[], cwd: string) {
|
|
1698
|
-
const command =
|
|
1699
|
-
manager === "npm" && args[0] === "add" ? "install" : args[0]!
|
|
1700
|
-
|
|
1701
|
-
const flags = args
|
|
1702
|
-
.slice(1)
|
|
1703
|
-
.map((argument) => (argument === "-d" ? "-D" : argument))
|
|
1704
|
-
|
|
1705
|
-
const child = Bun.spawn([manager, command, ...flags], {
|
|
1706
|
-
cwd,
|
|
1707
|
-
stdout: "inherit",
|
|
1708
|
-
stderr: "inherit",
|
|
1709
|
-
})
|
|
1871
|
+
let baseInstalled = false
|
|
1710
1872
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1873
|
+
try {
|
|
1874
|
+
await installItem("base", {
|
|
1875
|
+
...options,
|
|
1876
|
+
cwd: uiRoot,
|
|
1877
|
+
importPrefix: shared?.name,
|
|
1878
|
+
run: uiRun,
|
|
1879
|
+
})
|
|
1880
|
+
baseInstalled = true
|
|
1881
|
+
|
|
1882
|
+
if (
|
|
1883
|
+
!(await readFile(join(uiRoot, "src/styles/styles.css"), "utf8")).includes(
|
|
1884
|
+
"@stylex;",
|
|
1885
|
+
)
|
|
1886
|
+
) {
|
|
1887
|
+
throw new Error(
|
|
1888
|
+
"Add @stylex; to src/styles/styles.css before configuring StyleX",
|
|
1889
|
+
)
|
|
1890
|
+
}
|
|
1714
1891
|
|
|
1715
|
-
|
|
1892
|
+
if (!(await existingFile(root, "package.json")))
|
|
1893
|
+
throw new Error("Missing package.json after installation")
|
|
1894
|
+
const packages = await readFile(join(root, "package.json"), "utf8")
|
|
1895
|
+
const installed: JsonValue = JSON.parse(packages)
|
|
1716
1896
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1897
|
+
if (!object(installed))
|
|
1898
|
+
throw new Error("Invalid package.json after installation")
|
|
1899
|
+
|
|
1900
|
+
const available = Object.fromEntries(
|
|
1901
|
+
[installed.dependencies, installed.devDependencies].flatMap((entry) =>
|
|
1902
|
+
object(entry) ? Object.entries(entry) : [],
|
|
1903
|
+
),
|
|
1724
1904
|
)
|
|
1725
|
-
}
|
|
1726
1905
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1906
|
+
const neededRuntime = runtimeDependencies.filter((name) => {
|
|
1907
|
+
if (shared && name === `${shared.name}@workspace:*`) {
|
|
1908
|
+
const version = available[shared.name]
|
|
1729
1909
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1910
|
+
return (
|
|
1911
|
+
!isString(version) ||
|
|
1912
|
+
(version !== "*" && !version.startsWith("workspace:"))
|
|
1913
|
+
)
|
|
1914
|
+
}
|
|
1732
1915
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
),
|
|
1737
|
-
)
|
|
1916
|
+
const packageName = name.includes("@", 1)
|
|
1917
|
+
? name.slice(0, name.lastIndexOf("@"))
|
|
1918
|
+
: name
|
|
1738
1919
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
)
|
|
1920
|
+
return !(packageName in available)
|
|
1921
|
+
})
|
|
1742
1922
|
|
|
1743
|
-
|
|
1923
|
+
if (neededRuntime.length) await packageRun(["add", ...neededRuntime], root)
|
|
1744
1924
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1925
|
+
const neededDev = devDependencies.filter(
|
|
1926
|
+
(name) => !(name.split("@").slice(0, -1).join("@") in available),
|
|
1927
|
+
)
|
|
1748
1928
|
|
|
1749
|
-
|
|
1929
|
+
if (neededDev.length) await packageRun(["add", "-d", ...neededDev], root)
|
|
1930
|
+
|
|
1931
|
+
if (shared) {
|
|
1932
|
+
const path = `${relative(root, uiRoot).replaceAll("\\", "/")}/package.json`
|
|
1933
|
+
const planned = edits.get(path)
|
|
1934
|
+
|
|
1935
|
+
if (planned) {
|
|
1936
|
+
if (!(await existingFile(root, path)))
|
|
1937
|
+
throw new Error("Missing UI package.json after installation")
|
|
1938
|
+
const current = await readFile(join(uiRoot, "package.json"), "utf8")
|
|
1939
|
+
const before: JsonValue = JSON.parse(planned.before!)
|
|
1940
|
+
const after: JsonValue = JSON.parse(planned.after)
|
|
1941
|
+
const value: JsonValue = JSON.parse(current)
|
|
1942
|
+
|
|
1943
|
+
if (
|
|
1944
|
+
!object(before) ||
|
|
1945
|
+
!object(after) ||
|
|
1946
|
+
!object(value) ||
|
|
1947
|
+
value.name !== before.name ||
|
|
1948
|
+
JSON.stringify(value.exports) !== JSON.stringify(before.exports) ||
|
|
1949
|
+
JSON.stringify(value.sideEffects) !==
|
|
1950
|
+
JSON.stringify(before.sideEffects)
|
|
1951
|
+
)
|
|
1952
|
+
throw new Error("UI package exports changed during installation")
|
|
1750
1953
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1954
|
+
edits.set(path, {
|
|
1955
|
+
before: current,
|
|
1956
|
+
after: `${JSON.stringify({ ...value, exports: after.exports, sideEffects: after.sideEffects }, null, 2)}\n`,
|
|
1957
|
+
})
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1754
1960
|
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1961
|
+
const changes = new Map<string, FileChange>(edits)
|
|
1757
1962
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
}
|
|
1963
|
+
for (const [path, before] of retired) {
|
|
1964
|
+
changes.set(path, { before, after: null })
|
|
1965
|
+
}
|
|
1762
1966
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
}
|
|
1967
|
+
if (framework === "next") {
|
|
1968
|
+
if (!(await existingFile(root, "package.json")))
|
|
1969
|
+
throw new Error("Missing package.json after installation")
|
|
1970
|
+
const before = await readFile(join(root, "package.json"), "utf8")
|
|
1971
|
+
const latest: JsonValue = JSON.parse(before)
|
|
1769
1972
|
|
|
1770
|
-
|
|
1973
|
+
if (!object(latest))
|
|
1974
|
+
throw new Error("Invalid package.json after installation")
|
|
1771
1975
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
)
|
|
1976
|
+
if (JSON.stringify(latest.scripts) !== JSON.stringify(manifest.scripts)) {
|
|
1977
|
+
throw new Error("Next.js scripts changed during init")
|
|
1978
|
+
}
|
|
1776
1979
|
|
|
1777
|
-
|
|
1778
|
-
throw new Error("Invalid package.json after installation")
|
|
1980
|
+
const scripts = nextScripts(latest.scripts)
|
|
1779
1981
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1982
|
+
changes.set("package.json", {
|
|
1983
|
+
before,
|
|
1984
|
+
after:
|
|
1985
|
+
JSON.stringify(latest.scripts) === JSON.stringify(scripts)
|
|
1986
|
+
? before
|
|
1987
|
+
: `${JSON.stringify({ ...latest, scripts }, null, 2)}\n`,
|
|
1988
|
+
})
|
|
1782
1989
|
}
|
|
1783
1990
|
|
|
1784
|
-
|
|
1991
|
+
await writeFiles(root, changes)
|
|
1785
1992
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
)
|
|
1791
|
-
|
|
1993
|
+
return { framework, configured: edits.size }
|
|
1994
|
+
} catch (cause) {
|
|
1995
|
+
if (!baseInstalled) throw cause
|
|
1996
|
+
throw new Error(
|
|
1997
|
+
`${cause instanceof Error ? cause.message : String(cause)}\nDependencies may have changed (package.json, lockfiles, node_modules); package-manager changes were not rolled back.\nPrior init base install may have changed source files and ui.json; that install was not rolled back.`,
|
|
1998
|
+
{ cause },
|
|
1999
|
+
)
|
|
1792
2000
|
}
|
|
1793
|
-
|
|
1794
|
-
return { framework, configured: edits.size }
|
|
1795
2001
|
}
|