@yopem-ui/cli 0.0.2 → 0.1.1
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 +136 -21
- package/src/init.ts +546 -156
- package/src/install.ts +554 -61
- package/src/project.ts +394 -0
package/src/init.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
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 {
|
|
9
|
+
applyInstall,
|
|
8
10
|
existingFile,
|
|
9
|
-
installItem,
|
|
10
11
|
isRecord,
|
|
11
12
|
isString,
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
planInstall,
|
|
14
|
+
writeFiles,
|
|
15
|
+
} from "@yopem-ui/cli/install"
|
|
16
|
+
import {
|
|
17
|
+
isPackageName,
|
|
18
|
+
packageRunner,
|
|
19
|
+
pendingDependencies,
|
|
20
|
+
workspaceRoot,
|
|
21
|
+
} from "@yopem-ui/cli/project"
|
|
22
|
+
import { readFile, realpath } from "node:fs/promises"
|
|
23
|
+
import { join, relative, resolve } from "node:path"
|
|
24
|
+
import ts from "typescript-api"
|
|
14
25
|
|
|
15
26
|
type Framework =
|
|
16
27
|
| "vite"
|
|
@@ -20,7 +31,10 @@ type Framework =
|
|
|
20
31
|
| "next"
|
|
21
32
|
| "astro"
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
interface SharedUI {
|
|
35
|
+
name: string
|
|
36
|
+
source: string
|
|
37
|
+
}
|
|
24
38
|
|
|
25
39
|
interface Edit {
|
|
26
40
|
start: number
|
|
@@ -28,8 +42,28 @@ interface Edit {
|
|
|
28
42
|
text: string
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
export interface InitResult {
|
|
46
|
+
framework: Framework
|
|
47
|
+
configured: number
|
|
48
|
+
warnings?: string[]
|
|
49
|
+
preview?: {
|
|
50
|
+
files: {
|
|
51
|
+
path: string
|
|
52
|
+
action: "write" | "skip" | "delete" | "conflict"
|
|
53
|
+
reason?: string
|
|
54
|
+
}[]
|
|
55
|
+
dependencies: {
|
|
56
|
+
cwd: string
|
|
57
|
+
dependencies: string[]
|
|
58
|
+
devDependencies: string[]
|
|
59
|
+
}[]
|
|
60
|
+
prerequisites: string[]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
export interface InitOptions extends InstallOptions {
|
|
32
65
|
framework?: Framework
|
|
66
|
+
ui?: string
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
function object(value: JsonValue | undefined): value is JsonObject {
|
|
@@ -266,6 +300,19 @@ function addAlias(
|
|
|
266
300
|
) {
|
|
267
301
|
throw new Error(`Incompatible @ alias in ${path}`)
|
|
268
302
|
}
|
|
303
|
+
|
|
304
|
+
const aliasValue = declaration?.initializer ?? current.initializer
|
|
305
|
+
|
|
306
|
+
if (
|
|
307
|
+
ts.isStringLiteral(aliasValue) &&
|
|
308
|
+
/^(?:\.\/)?src\/?$/.test(aliasValue.text)
|
|
309
|
+
) {
|
|
310
|
+
edits.push({
|
|
311
|
+
start: current.initializer.getStart(source),
|
|
312
|
+
end: current.initializer.end,
|
|
313
|
+
text: "yopemSource",
|
|
314
|
+
})
|
|
315
|
+
}
|
|
269
316
|
}
|
|
270
317
|
|
|
271
318
|
function moduleConfig(source: ts.SourceFile, path: string) {
|
|
@@ -549,14 +596,18 @@ function babelPlugin(presets = false) {
|
|
|
549
596
|
return `babel({ plugins: yopemBabelPlugins${presets ? ", presets: yopemBabelPresets" : ""} })`
|
|
550
597
|
}
|
|
551
598
|
|
|
552
|
-
function stylexSetup(
|
|
599
|
+
function stylexSetup(
|
|
600
|
+
babelPlugins: string[] = [],
|
|
601
|
+
babelPresets: string[] = [],
|
|
602
|
+
shared?: SharedUI,
|
|
603
|
+
) {
|
|
553
604
|
return `import babel from "@rolldown/plugin-babel"
|
|
554
605
|
import { createRequire as yopemCreateRequire } from "node:module"
|
|
555
606
|
import { fileURLToPath as yopemFileURLToPath } from "node:url"
|
|
556
607
|
|
|
557
|
-
const yopemSource = yopemFileURLToPath(new URL("./src", import.meta.url))
|
|
608
|
+
const yopemSource = yopemFileURLToPath(new URL("./src", import.meta.url))${shared ? `\nconst yopemUISource = yopemFileURLToPath(new URL(${JSON.stringify(shared.source)}, import.meta.url))` : ""}
|
|
558
609
|
const yopemBabelPlugins = [${babelPlugins.join(", ")}${babelPlugins.length ? ", " : ""}["@stylexjs/babel-plugin", {
|
|
559
|
-
aliases: { "@/*": [yopemSource + "/*"] },
|
|
610
|
+
aliases: { "@/*": [yopemSource + "/*"]${shared ? `, ${JSON.stringify(`${shared.name}/*`)}: [yopemUISource + "/*"]` : ""} },
|
|
560
611
|
dev: process.env.NODE_ENV !== "production",
|
|
561
612
|
runtimeInjection: false,
|
|
562
613
|
treeshakeCompensation: true,
|
|
@@ -564,7 +615,7 @@ const yopemBabelPlugins = [${babelPlugins.join(", ")}${babelPlugins.length ? ",
|
|
|
564
615
|
}]]${babelPresets.length ? `\nconst yopemBabelPresets = [${babelPresets.join(", ")}]` : ""}
|
|
565
616
|
const yopemPostcssPlugin = yopemCreateRequire(import.meta.url)("@stylexjs/postcss-plugin")({
|
|
566
617
|
cwd: yopemFileURLToPath(new URL(".", import.meta.url)),
|
|
567
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}"],
|
|
618
|
+
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}"${shared ? ', yopemUISource + "/**/*.{js,jsx,ts,tsx}"' : ""}],
|
|
568
619
|
babelConfig: {
|
|
569
620
|
babelrc: false,
|
|
570
621
|
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
@@ -697,10 +748,20 @@ function viteConfig(
|
|
|
697
748
|
path: string,
|
|
698
749
|
babelConfig: ReturnType<typeof migrateBabel>,
|
|
699
750
|
postcssPlugins: string[],
|
|
751
|
+
shared?: SharedUI,
|
|
700
752
|
) {
|
|
701
753
|
content = stripLegacyConfig(content, path)
|
|
702
754
|
|
|
703
755
|
if (content.includes("stylex.vite(") || hasYopemStylexConfig(content)) {
|
|
756
|
+
if (
|
|
757
|
+
shared &&
|
|
758
|
+
(!content.includes(JSON.stringify(`${shared.name}/*`)) ||
|
|
759
|
+
!content.includes(JSON.stringify(shared.source)))
|
|
760
|
+
)
|
|
761
|
+
throw new Error(
|
|
762
|
+
`Existing StyleX configuration targets a different UI package in ${path}`,
|
|
763
|
+
)
|
|
764
|
+
|
|
704
765
|
if (!content.includes('"@":') && !content.includes("'@':")) {
|
|
705
766
|
throw new Error(`Existing StyleX configuration lacks @ alias in ${path}`)
|
|
706
767
|
}
|
|
@@ -736,7 +797,7 @@ function viteConfig(
|
|
|
736
797
|
edits.push({
|
|
737
798
|
start: 0,
|
|
738
799
|
end: 0,
|
|
739
|
-
text: stylexSetup(babelConfig.plugins, babelConfig.presets),
|
|
800
|
+
text: stylexSetup(babelConfig.plugins, babelConfig.presets, shared),
|
|
740
801
|
})
|
|
741
802
|
|
|
742
803
|
return applyEdits(content, edits)
|
|
@@ -747,6 +808,7 @@ function astroConfig(
|
|
|
747
808
|
path: string,
|
|
748
809
|
babelConfig: ReturnType<typeof migrateBabel>,
|
|
749
810
|
postcssPlugins: string[],
|
|
811
|
+
shared?: SharedUI,
|
|
750
812
|
) {
|
|
751
813
|
content = stripLegacyConfig(content, path)
|
|
752
814
|
const source = parsed(path, content)
|
|
@@ -772,6 +834,15 @@ function astroConfig(
|
|
|
772
834
|
hasYopemStylexConfig(content) && content.includes(`${reactName}()`)
|
|
773
835
|
|
|
774
836
|
if (alreadyConfigured) {
|
|
837
|
+
if (
|
|
838
|
+
shared &&
|
|
839
|
+
(!content.includes(JSON.stringify(`${shared.name}/*`)) ||
|
|
840
|
+
!content.includes(JSON.stringify(shared.source)))
|
|
841
|
+
)
|
|
842
|
+
throw new Error(
|
|
843
|
+
`Existing StyleX configuration targets a different UI package in ${path}`,
|
|
844
|
+
)
|
|
845
|
+
|
|
775
846
|
if (!content.includes('"@":') && !content.includes("'@':")) {
|
|
776
847
|
throw new Error(`Existing StyleX configuration lacks @ alias in ${path}`)
|
|
777
848
|
}
|
|
@@ -847,7 +918,7 @@ function astroConfig(
|
|
|
847
918
|
edits.push({
|
|
848
919
|
start: 0,
|
|
849
920
|
end: 0,
|
|
850
|
-
text: `${reactImport ? "" : 'import react from "@astrojs/react"\n'}${stylexSetup(babelConfig.plugins, babelConfig.presets)}`,
|
|
921
|
+
text: `${reactImport ? "" : 'import react from "@astrojs/react"\n'}${stylexSetup(babelConfig.plugins, babelConfig.presets, shared)}`,
|
|
851
922
|
})
|
|
852
923
|
|
|
853
924
|
return applyEdits(content, edits)
|
|
@@ -924,10 +995,10 @@ function tsconfig(content: string, path: string) {
|
|
|
924
995
|
return applyEdits(content, edits)
|
|
925
996
|
}
|
|
926
997
|
|
|
927
|
-
function reactEntry(content: string, path: string) {
|
|
998
|
+
function reactEntry(content: string, path: string, prefix = "@") {
|
|
928
999
|
if (content.includes("themeMarker") || content.includes("rootStyles.html")) {
|
|
929
1000
|
if (
|
|
930
|
-
!content.includes(
|
|
1001
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
931
1002
|
!content.includes("rootStyles.html") ||
|
|
932
1003
|
!content.includes('dataset.theme = "light"')
|
|
933
1004
|
) {
|
|
@@ -947,7 +1018,7 @@ function reactEntry(content: string, path: string) {
|
|
|
947
1018
|
{
|
|
948
1019
|
start: 0,
|
|
949
1020
|
end: 0,
|
|
950
|
-
text:
|
|
1021
|
+
text: `import "${prefix}/styles/styles.css"\nimport * as stylex from "@stylexjs/stylex"\nimport { lightTheme, rootStyles, themeMarker } from "${prefix}/styles/tokens.stylex"\n`,
|
|
951
1022
|
},
|
|
952
1023
|
{
|
|
953
1024
|
start: last.end,
|
|
@@ -961,10 +1032,11 @@ function jsxLayout(
|
|
|
961
1032
|
content: string,
|
|
962
1033
|
path: string,
|
|
963
1034
|
framework: "next" | "tanstack-start" | "react-router",
|
|
1035
|
+
prefix = "@",
|
|
964
1036
|
) {
|
|
965
1037
|
if (content.includes("stylexProps(") || content.includes("rootStyles.html")) {
|
|
966
1038
|
if (
|
|
967
|
-
!content.includes(
|
|
1039
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
968
1040
|
!content.includes("rootStyles.html") ||
|
|
969
1041
|
!content.includes("rootStyles.body")
|
|
970
1042
|
) {
|
|
@@ -1064,20 +1136,20 @@ function jsxLayout(
|
|
|
1064
1136
|
})
|
|
1065
1137
|
}
|
|
1066
1138
|
|
|
1067
|
-
const css =
|
|
1139
|
+
const css = `import "${prefix}/styles/styles.css"\n`
|
|
1068
1140
|
edits.push({
|
|
1069
1141
|
start: 0,
|
|
1070
1142
|
end: 0,
|
|
1071
|
-
text: `${css}import { stylexProps } from "
|
|
1143
|
+
text: `${css}import { stylexProps } from "${prefix}/lib/stylex"\nimport { lightTheme, rootStyles, themeMarker } from "${prefix}/styles/tokens.stylex"\n`,
|
|
1072
1144
|
})
|
|
1073
1145
|
|
|
1074
1146
|
return applyEdits(content, edits)
|
|
1075
1147
|
}
|
|
1076
1148
|
|
|
1077
|
-
function astroLayout(content: string, path: string) {
|
|
1149
|
+
function astroLayout(content: string, path: string, prefix = "@") {
|
|
1078
1150
|
if (content.includes("yopemHtml.className")) {
|
|
1079
1151
|
if (
|
|
1080
|
-
!content.includes(
|
|
1152
|
+
!content.includes(`"${prefix}/styles/styles.css"`) ||
|
|
1081
1153
|
!content.includes("yopemBody.className")
|
|
1082
1154
|
) {
|
|
1083
1155
|
throw new Error(`Incomplete Yopem layout in ${path}`)
|
|
@@ -1117,7 +1189,7 @@ function astroLayout(content: string, path: string) {
|
|
|
1117
1189
|
{
|
|
1118
1190
|
start: end,
|
|
1119
1191
|
end,
|
|
1120
|
-
text: `${content.includes(
|
|
1192
|
+
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
1193
|
},
|
|
1122
1194
|
]
|
|
1123
1195
|
|
|
@@ -1170,7 +1242,7 @@ function astroLayout(content: string, path: string) {
|
|
|
1170
1242
|
return applyEdits(content, edits)
|
|
1171
1243
|
}
|
|
1172
1244
|
|
|
1173
|
-
function nextBabel(esm: boolean) {
|
|
1245
|
+
function nextBabel(esm: boolean, shared?: SharedUI) {
|
|
1174
1246
|
const header = esm
|
|
1175
1247
|
? `import { createRequire } from "node:module"
|
|
1176
1248
|
import { dirname } from "node:path"
|
|
@@ -1208,7 +1280,7 @@ function expandLocalSpreads({ types: t }) {
|
|
|
1208
1280
|
}
|
|
1209
1281
|
|
|
1210
1282
|
${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/*")] },
|
|
1283
|
+
aliases: { "@/*": [path.join(__dirname, "src/*")]${shared ? `, ${JSON.stringify(`${shared.name}/*`)}: [path.join(__dirname, ${JSON.stringify(`${shared.source}*`)})]` : ""} },
|
|
1212
1284
|
dev: process.env.NODE_ENV !== "production",
|
|
1213
1285
|
runtimeInjection: false,
|
|
1214
1286
|
treeshakeCompensation: true,
|
|
@@ -1217,7 +1289,40 @@ ${esm ? 'export const presets = ["next/babel"]\nexport const plugins = [' : 'mod
|
|
|
1217
1289
|
`
|
|
1218
1290
|
}
|
|
1219
1291
|
|
|
1220
|
-
function
|
|
1292
|
+
function nextConfig(content: string, path: string, name: string) {
|
|
1293
|
+
const source = parsed(path, content)
|
|
1294
|
+
const config = moduleConfig(source, path)
|
|
1295
|
+
const packages = property(config, "transpilePackages")
|
|
1296
|
+
const edits: Edit[] = []
|
|
1297
|
+
|
|
1298
|
+
if (!packages) {
|
|
1299
|
+
addProperty(
|
|
1300
|
+
source,
|
|
1301
|
+
config,
|
|
1302
|
+
`transpilePackages: [${JSON.stringify(name)}]`,
|
|
1303
|
+
edits,
|
|
1304
|
+
)
|
|
1305
|
+
} else {
|
|
1306
|
+
if (!ts.isArrayLiteralExpression(packages.initializer))
|
|
1307
|
+
throw new Error(`Unsupported transpilePackages in ${path}`)
|
|
1308
|
+
|
|
1309
|
+
if (
|
|
1310
|
+
!packages.initializer.elements.some(
|
|
1311
|
+
(entry) => ts.isStringLiteral(entry) && entry.text === name,
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
addArrayEntries(
|
|
1315
|
+
source,
|
|
1316
|
+
packages.initializer,
|
|
1317
|
+
[JSON.stringify(name)],
|
|
1318
|
+
edits,
|
|
1319
|
+
)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return applyEdits(content, edits)
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function nextPostcss(shared?: SharedUI) {
|
|
1221
1326
|
return `// Next loads this PostCSS config synchronously through CommonJS.
|
|
1222
1327
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1223
1328
|
const babelConfig = require("./babel.config.js")
|
|
@@ -1225,7 +1330,7 @@ const babelConfig = require("./babel.config.js")
|
|
|
1225
1330
|
module.exports = {
|
|
1226
1331
|
plugins: {
|
|
1227
1332
|
"@stylexjs/postcss-plugin": {
|
|
1228
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"],
|
|
1333
|
+
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
1334
|
babelConfig: {
|
|
1230
1335
|
babelrc: false,
|
|
1231
1336
|
parserOpts: { plugins: ["typescript", "jsx"] },
|
|
@@ -1239,7 +1344,12 @@ module.exports = {
|
|
|
1239
1344
|
`
|
|
1240
1345
|
}
|
|
1241
1346
|
|
|
1242
|
-
function postcss(
|
|
1347
|
+
function postcss(
|
|
1348
|
+
content: string,
|
|
1349
|
+
path: string,
|
|
1350
|
+
esm: boolean,
|
|
1351
|
+
shared?: SharedUI,
|
|
1352
|
+
) {
|
|
1243
1353
|
if (content.includes('"@stylexjs/postcss-plugin"')) return content
|
|
1244
1354
|
const source = parsed(path, content)
|
|
1245
1355
|
let config: ts.ObjectLiteralExpression | undefined
|
|
@@ -1269,7 +1379,7 @@ function postcss(content: string, path: string, esm: boolean) {
|
|
|
1269
1379
|
source,
|
|
1270
1380
|
plugins,
|
|
1271
1381
|
`"@stylexjs/postcss-plugin": {
|
|
1272
|
-
include: ["src/**/*.{js,jsx,ts,tsx}", "app/**/*.{js,jsx,ts,tsx}", "pages/**/*.{js,jsx,ts,tsx}"],
|
|
1382
|
+
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
1383
|
babelConfig: { babelrc: false, parserOpts: { plugins: ["typescript", "jsx"] }, plugins: yopemBabelConfig.plugins },
|
|
1274
1384
|
useCSSLayers: true,
|
|
1275
1385
|
}`,
|
|
@@ -1279,7 +1389,7 @@ function postcss(content: string, path: string, esm: boolean) {
|
|
|
1279
1389
|
start: 0,
|
|
1280
1390
|
end: 0,
|
|
1281
1391
|
text: esm
|
|
1282
|
-
? 'import { createRequire } from "node:module"\nconst
|
|
1392
|
+
? 'import { createRequire } from "node:module"\nconst yopemRequire = createRequire(import.meta.url)\nconst yopemBabelConfig = yopemRequire("./babel.config.js")\n'
|
|
1283
1393
|
: 'const yopemBabelConfig = require("./babel.config.js")\n',
|
|
1284
1394
|
})
|
|
1285
1395
|
|
|
@@ -1295,7 +1405,7 @@ const lintRules = {
|
|
|
1295
1405
|
"yopem-ui/valid-polymorphic-as": "error",
|
|
1296
1406
|
}
|
|
1297
1407
|
|
|
1298
|
-
function lintConfig(content: string) {
|
|
1408
|
+
function lintConfig(content: string, shared?: SharedUI) {
|
|
1299
1409
|
const config: JsonValue = JSON.parse(content)
|
|
1300
1410
|
|
|
1301
1411
|
if (!object(config)) throw new Error("Invalid .oxlintrc.json")
|
|
@@ -1325,6 +1435,45 @@ function lintConfig(content: string) {
|
|
|
1325
1435
|
throw new Error("Conflicting yopem-ui plugin in .oxlintrc.json")
|
|
1326
1436
|
}
|
|
1327
1437
|
|
|
1438
|
+
const configuredRules: JsonObject = { ...lintRules, ...rules }
|
|
1439
|
+
|
|
1440
|
+
if (shared) {
|
|
1441
|
+
for (const name of [
|
|
1442
|
+
"yopem-ui/enforce-styling-methods",
|
|
1443
|
+
"yopem-ui/no-restyle",
|
|
1444
|
+
"yopem-ui/valid-polymorphic-as",
|
|
1445
|
+
]) {
|
|
1446
|
+
const setting = configuredRules[name] ?? "error"
|
|
1447
|
+
const level = Array.isArray(setting) ? (setting[0] ?? "error") : setting
|
|
1448
|
+
|
|
1449
|
+
if (level === "off" || level === 0) continue
|
|
1450
|
+
|
|
1451
|
+
const options = Array.isArray(setting) ? (setting[1] ?? {}) : {}
|
|
1452
|
+
|
|
1453
|
+
if (!object(options)) throw new Error(`Invalid options for ${name}`)
|
|
1454
|
+
|
|
1455
|
+
const sources = options.componentSources ?? [
|
|
1456
|
+
"@/components/ui/",
|
|
1457
|
+
"@registry/components/ui/",
|
|
1458
|
+
"@yopem-ui/ui",
|
|
1459
|
+
]
|
|
1460
|
+
|
|
1461
|
+
if (!Array.isArray(sources) || !sources.every(isString)) {
|
|
1462
|
+
throw new Error(`Invalid componentSources for ${name}`)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
configuredRules[name] = [
|
|
1466
|
+
level,
|
|
1467
|
+
{
|
|
1468
|
+
...options,
|
|
1469
|
+
componentSources: [
|
|
1470
|
+
...new Set([...sources, `${shared.name}/components/ui/`]),
|
|
1471
|
+
],
|
|
1472
|
+
},
|
|
1473
|
+
]
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1328
1477
|
const next = {
|
|
1329
1478
|
...config,
|
|
1330
1479
|
jsPlugins: existing
|
|
@@ -1333,7 +1482,7 @@ function lintConfig(content: string) {
|
|
|
1333
1482
|
...plugins,
|
|
1334
1483
|
{ name: "yopem-ui", specifier: "@yopem-ui/oxlint-plugin" },
|
|
1335
1484
|
],
|
|
1336
|
-
rules:
|
|
1485
|
+
rules: configuredRules,
|
|
1337
1486
|
overrides: overrides.some(
|
|
1338
1487
|
(entry) => JSON.stringify(entry) === JSON.stringify(componentOverride),
|
|
1339
1488
|
)
|
|
@@ -1419,48 +1568,9 @@ function detectFramework(
|
|
|
1419
1568
|
return detected[0]!
|
|
1420
1569
|
}
|
|
1421
1570
|
|
|
1422
|
-
async function
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
export async function initProject(options: InitOptions = {}) {
|
|
1571
|
+
export async function initProject(
|
|
1572
|
+
options: InitOptions = {},
|
|
1573
|
+
): Promise<InitResult> {
|
|
1464
1574
|
const root = await realpath(options.cwd ?? process.cwd())
|
|
1465
1575
|
|
|
1466
1576
|
if (!(await existingFile(root, "package.json"))) {
|
|
@@ -1484,13 +1594,46 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1484
1594
|
)
|
|
1485
1595
|
|
|
1486
1596
|
const framework = detectFramework(dependencies, options.framework)
|
|
1487
|
-
const
|
|
1597
|
+
const uiRoot = options.ui ? await realpath(resolve(root, options.ui)) : root
|
|
1598
|
+
let shared: SharedUI | undefined
|
|
1599
|
+
|
|
1600
|
+
if (options.ui) {
|
|
1601
|
+
if (uiRoot === root)
|
|
1602
|
+
throw new Error("Shared UI package must differ from app")
|
|
1603
|
+
|
|
1604
|
+
const uiManifest: JsonValue = JSON.parse(
|
|
1605
|
+
await readFile(join(uiRoot, "package.json"), "utf8"),
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
if (!object(uiManifest) || !isPackageName(uiManifest.name))
|
|
1609
|
+
throw new Error("Shared UI package requires a valid package name")
|
|
1610
|
+
|
|
1611
|
+
const owner = await workspaceRoot(root)
|
|
1612
|
+
|
|
1613
|
+
if ((await workspaceRoot(uiRoot)) !== owner)
|
|
1614
|
+
throw new Error("App and UI package must belong to the same workspace")
|
|
1615
|
+
|
|
1616
|
+
shared = {
|
|
1617
|
+
name: uiManifest.name,
|
|
1618
|
+
source: `${relative(root, join(uiRoot, "src")).replaceAll("\\", "/")}/`,
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const packageRun = await packageRunner(root, options.run)
|
|
1623
|
+
const uiRun = await packageRunner(uiRoot, options.run)
|
|
1488
1624
|
const edits = new Map<string, { before: string | null; after: string }>()
|
|
1489
1625
|
|
|
1626
|
+
const plannedFiles: {
|
|
1627
|
+
path: string
|
|
1628
|
+
action: "write" | "skip" | "delete"
|
|
1629
|
+
reason?: string
|
|
1630
|
+
}[] = []
|
|
1631
|
+
|
|
1490
1632
|
async function plan(
|
|
1491
1633
|
path: string,
|
|
1492
1634
|
transform: (content: string) => string,
|
|
1493
1635
|
fallback?: string,
|
|
1636
|
+
reason?: string,
|
|
1494
1637
|
) {
|
|
1495
1638
|
const exists = await existingFile(root, path)
|
|
1496
1639
|
|
|
@@ -1498,6 +1641,12 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1498
1641
|
const before = exists ? await readFile(join(root, path), "utf8") : null
|
|
1499
1642
|
const after = transform(before ?? fallback!)
|
|
1500
1643
|
|
|
1644
|
+
plannedFiles.push({
|
|
1645
|
+
path: resolve(root, path),
|
|
1646
|
+
action: before === after ? "skip" : "write",
|
|
1647
|
+
...(reason && { reason }),
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1501
1650
|
if (before !== after) edits.set(path, { before, after })
|
|
1502
1651
|
}
|
|
1503
1652
|
|
|
@@ -1550,7 +1699,54 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1550
1699
|
: "tsconfig.json"
|
|
1551
1700
|
|
|
1552
1701
|
await plan(tsPath, (content) => tsconfig(content, tsPath))
|
|
1553
|
-
await plan(".oxlintrc.json", lintConfig, "{}")
|
|
1702
|
+
await plan(".oxlintrc.json", (content) => lintConfig(content, shared), "{}")
|
|
1703
|
+
|
|
1704
|
+
if (shared) {
|
|
1705
|
+
const prefix = relative(root, uiRoot).replaceAll("\\", "/")
|
|
1706
|
+
|
|
1707
|
+
await plan(
|
|
1708
|
+
`${prefix}/package.json`,
|
|
1709
|
+
(content) => {
|
|
1710
|
+
const value: JsonValue = JSON.parse(content)
|
|
1711
|
+
|
|
1712
|
+
if (
|
|
1713
|
+
!object(value) ||
|
|
1714
|
+
(value.exports !== undefined && !object(value.exports))
|
|
1715
|
+
)
|
|
1716
|
+
throw new Error("Shared UI package requires object exports")
|
|
1717
|
+
const exports = object(value.exports) ? { ...value.exports } : {}
|
|
1718
|
+
|
|
1719
|
+
const paths = {
|
|
1720
|
+
"./components/ui/*": "./src/components/ui/*.tsx",
|
|
1721
|
+
"./lib/*": "./src/lib/*.ts",
|
|
1722
|
+
"./styles/tokens.stylex": "./src/styles/tokens.stylex.ts",
|
|
1723
|
+
"./styles/styles.css": "./src/styles/styles.css",
|
|
1724
|
+
"./theme/*": "./src/theme/*.tsx",
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
for (const [path, target] of Object.entries(paths)) {
|
|
1728
|
+
if (exports[path] !== undefined && exports[path] !== target)
|
|
1729
|
+
throw new Error(`Conflicting UI package export: ${path}`)
|
|
1730
|
+
exports[path] = target
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const sideEffects =
|
|
1734
|
+
value.sideEffects === false
|
|
1735
|
+
? ["**/*.css"]
|
|
1736
|
+
: Array.isArray(value.sideEffects) &&
|
|
1737
|
+
!value.sideEffects.includes("**/*.css")
|
|
1738
|
+
? [...value.sideEffects, "**/*.css"]
|
|
1739
|
+
: value.sideEffects
|
|
1740
|
+
|
|
1741
|
+
return JSON.stringify(value.exports) === JSON.stringify(exports) &&
|
|
1742
|
+
JSON.stringify(value.sideEffects) === JSON.stringify(sideEffects)
|
|
1743
|
+
? content
|
|
1744
|
+
: `${JSON.stringify({ ...value, exports, sideEffects }, null, 2)}\n`
|
|
1745
|
+
},
|
|
1746
|
+
undefined,
|
|
1747
|
+
"Shared UI exports: ./components/ui/*, ./lib/*, ./styles/tokens.stylex, ./styles/styles.css, ./theme/*; CSS side effects",
|
|
1748
|
+
)
|
|
1749
|
+
}
|
|
1554
1750
|
|
|
1555
1751
|
const devDependencies = [
|
|
1556
1752
|
"oxlint@^1.79.0",
|
|
@@ -1561,7 +1757,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1561
1757
|
"@stylexjs/postcss-plugin@^0.19.0",
|
|
1562
1758
|
]
|
|
1563
1759
|
|
|
1564
|
-
const runtimeDependencies: string[] =
|
|
1760
|
+
const runtimeDependencies: string[] = shared
|
|
1761
|
+
? [`${shared.name}@workspace:*`, "@stylexjs/stylex@^0.19.0"]
|
|
1762
|
+
: []
|
|
1565
1763
|
|
|
1566
1764
|
if (framework === "astro") {
|
|
1567
1765
|
const config = await chooseFile(root, [
|
|
@@ -1571,7 +1769,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1571
1769
|
])
|
|
1572
1770
|
|
|
1573
1771
|
await plan(config, (content) =>
|
|
1574
|
-
astroConfig(content, config, babelConfig, postcssPlugins),
|
|
1772
|
+
astroConfig(content, config, babelConfig, postcssPlugins, shared),
|
|
1575
1773
|
)
|
|
1576
1774
|
|
|
1577
1775
|
const layout = await chooseFile(root, [
|
|
@@ -1579,12 +1777,27 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1579
1777
|
"src/layouts/layout.astro",
|
|
1580
1778
|
])
|
|
1581
1779
|
|
|
1582
|
-
await plan(layout, (content) => astroLayout(content, layout))
|
|
1780
|
+
await plan(layout, (content) => astroLayout(content, layout, shared?.name))
|
|
1583
1781
|
|
|
1584
1782
|
for (const name of ["@astrojs/react", "react", "react-dom"]) {
|
|
1585
1783
|
if (!(name in dependencies)) runtimeDependencies.push(name)
|
|
1586
1784
|
}
|
|
1587
1785
|
} else if (framework === "next") {
|
|
1786
|
+
if (shared) {
|
|
1787
|
+
const config = await chooseFile(
|
|
1788
|
+
root,
|
|
1789
|
+
["next.config.ts", "next.config.mjs", "next.config.js"],
|
|
1790
|
+
"next.config.mjs",
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
const name = shared.name
|
|
1794
|
+
await plan(
|
|
1795
|
+
config,
|
|
1796
|
+
(content) => nextConfig(content, config, name),
|
|
1797
|
+
"export default {}\n",
|
|
1798
|
+
)
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1588
1801
|
if (!object(manifest.scripts))
|
|
1589
1802
|
throw new Error("Next.js scripts are missing")
|
|
1590
1803
|
nextScripts(manifest.scripts)
|
|
@@ -1594,7 +1807,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1594
1807
|
"app/layout.tsx",
|
|
1595
1808
|
])
|
|
1596
1809
|
|
|
1597
|
-
await plan(layout, (content) =>
|
|
1810
|
+
await plan(layout, (content) =>
|
|
1811
|
+
jsxLayout(content, layout, "next", shared?.name),
|
|
1812
|
+
)
|
|
1598
1813
|
|
|
1599
1814
|
const babel = await chooseFile(
|
|
1600
1815
|
root,
|
|
@@ -1612,7 +1827,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1612
1827
|
throw new Error(`Unsupported Babel configuration: ${babel}`)
|
|
1613
1828
|
}
|
|
1614
1829
|
|
|
1615
|
-
const babelContent = nextBabel(manifest.type === "module")
|
|
1830
|
+
const babelContent = nextBabel(manifest.type === "module", shared)
|
|
1616
1831
|
await plan(
|
|
1617
1832
|
babel,
|
|
1618
1833
|
(content) => {
|
|
@@ -1634,15 +1849,16 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1634
1849
|
await plan(
|
|
1635
1850
|
postcssPath,
|
|
1636
1851
|
(content) =>
|
|
1637
|
-
postcssPath === "postcss.config.cjs" && content === nextPostcss()
|
|
1852
|
+
postcssPath === "postcss.config.cjs" && content === nextPostcss(shared)
|
|
1638
1853
|
? content
|
|
1639
1854
|
: postcss(
|
|
1640
1855
|
content,
|
|
1641
1856
|
postcssPath,
|
|
1642
1857
|
postcssPath.endsWith(".mjs") ||
|
|
1643
1858
|
(postcssPath.endsWith(".js") && manifest.type === "module"),
|
|
1859
|
+
shared,
|
|
1644
1860
|
),
|
|
1645
|
-
nextPostcss(),
|
|
1861
|
+
nextPostcss(shared),
|
|
1646
1862
|
)
|
|
1647
1863
|
devDependencies.splice(
|
|
1648
1864
|
0,
|
|
@@ -1663,7 +1879,7 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1663
1879
|
])
|
|
1664
1880
|
|
|
1665
1881
|
await plan(config, (content) =>
|
|
1666
|
-
viteConfig(content, config, babelConfig, postcssPlugins),
|
|
1882
|
+
viteConfig(content, config, babelConfig, postcssPlugins, shared),
|
|
1667
1883
|
)
|
|
1668
1884
|
|
|
1669
1885
|
if (
|
|
@@ -1677,7 +1893,9 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1677
1893
|
: ["app/root.tsx", "src/root.tsx"],
|
|
1678
1894
|
)
|
|
1679
1895
|
|
|
1680
|
-
await plan(layout, (content) =>
|
|
1896
|
+
await plan(layout, (content) =>
|
|
1897
|
+
jsxLayout(content, layout, framework, shared?.name),
|
|
1898
|
+
)
|
|
1681
1899
|
} else {
|
|
1682
1900
|
const entry = await chooseFile(root, [
|
|
1683
1901
|
"src/main.tsx",
|
|
@@ -1686,110 +1904,282 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1686
1904
|
"src/index.jsx",
|
|
1687
1905
|
])
|
|
1688
1906
|
|
|
1689
|
-
await plan(entry, (content) => reactEntry(content, entry))
|
|
1907
|
+
await plan(entry, (content) => reactEntry(content, entry, shared?.name))
|
|
1690
1908
|
}
|
|
1691
1909
|
}
|
|
1692
1910
|
|
|
1693
|
-
const
|
|
1694
|
-
options
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
})
|
|
1911
|
+
const basePlan = await planInstall("base", {
|
|
1912
|
+
...options,
|
|
1913
|
+
cwd: uiRoot,
|
|
1914
|
+
importPrefix: shared?.name,
|
|
1915
|
+
dryRun: true,
|
|
1916
|
+
run: uiRun,
|
|
1917
|
+
})
|
|
1710
1918
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1919
|
+
const conflict = basePlan.preview.files.find(
|
|
1920
|
+
(file) => file.action === "conflict",
|
|
1921
|
+
)
|
|
1714
1922
|
|
|
1715
|
-
|
|
1923
|
+
if (conflict) throw new Error(conflict.reason)
|
|
1716
1924
|
|
|
1717
1925
|
if (
|
|
1718
|
-
!(
|
|
1719
|
-
"@stylex;",
|
|
1720
|
-
)
|
|
1926
|
+
!basePlan.changes.get("src/styles/styles.css")?.after?.includes("@stylex;")
|
|
1721
1927
|
) {
|
|
1722
1928
|
throw new Error(
|
|
1723
1929
|
"Add @stylex; to src/styles/styles.css before configuring StyleX",
|
|
1724
1930
|
)
|
|
1725
1931
|
}
|
|
1726
1932
|
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1933
|
+
const runtimeRequirements =
|
|
1934
|
+
uiRoot === root
|
|
1935
|
+
? [...basePlan.requirements.dependencies, ...runtimeDependencies]
|
|
1936
|
+
: runtimeDependencies
|
|
1729
1937
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1938
|
+
const devRequirements =
|
|
1939
|
+
uiRoot === root
|
|
1940
|
+
? [...basePlan.requirements.devDependencies, ...devDependencies]
|
|
1941
|
+
: devDependencies
|
|
1732
1942
|
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1943
|
+
const appPending = await pendingDependencies(
|
|
1944
|
+
root,
|
|
1945
|
+
runtimeRequirements,
|
|
1946
|
+
devRequirements,
|
|
1737
1947
|
)
|
|
1738
1948
|
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1949
|
+
const baseDependencyNames = new Set(
|
|
1950
|
+
[
|
|
1951
|
+
...basePlan.requirements.dependencies,
|
|
1952
|
+
...basePlan.requirements.devDependencies,
|
|
1953
|
+
].map((argument) => {
|
|
1954
|
+
const separator = argument.indexOf("@", 1)
|
|
1955
|
+
|
|
1956
|
+
return separator < 0 ? argument : argument.slice(0, separator)
|
|
1957
|
+
}),
|
|
1741
1958
|
)
|
|
1742
1959
|
|
|
1743
|
-
|
|
1960
|
+
function isBaseDependency(argument: string) {
|
|
1961
|
+
const separator = argument.indexOf("@", 1)
|
|
1744
1962
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1963
|
+
return baseDependencyNames.has(
|
|
1964
|
+
separator < 0 ? argument : argument.slice(0, separator),
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1748
1967
|
|
|
1749
|
-
|
|
1968
|
+
const baseApplicationPlan =
|
|
1969
|
+
uiRoot === root
|
|
1970
|
+
? {
|
|
1971
|
+
...basePlan,
|
|
1972
|
+
preview: {
|
|
1973
|
+
...basePlan.preview,
|
|
1974
|
+
dependencies: appPending.dependencies.filter(isBaseDependency),
|
|
1975
|
+
devDependencies:
|
|
1976
|
+
appPending.devDependencies.filter(isBaseDependency),
|
|
1977
|
+
},
|
|
1978
|
+
}
|
|
1979
|
+
: basePlan
|
|
1750
1980
|
|
|
1751
|
-
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1981
|
+
if (options.dryRun) {
|
|
1982
|
+
const baseDependencies = {
|
|
1983
|
+
dependencies: basePlan.preview.dependencies,
|
|
1984
|
+
devDependencies: basePlan.preview.devDependencies,
|
|
1985
|
+
}
|
|
1754
1986
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1987
|
+
for (const [path] of retired)
|
|
1988
|
+
plannedFiles.push({ path: resolve(root, path), action: "delete" })
|
|
1989
|
+
|
|
1990
|
+
if (framework === "next") {
|
|
1991
|
+
if (!object(manifest.scripts))
|
|
1992
|
+
throw new Error("Next.js scripts are missing")
|
|
1993
|
+
const originalScripts = manifest.scripts
|
|
1994
|
+
const scripts = nextScripts(originalScripts)
|
|
1995
|
+
plannedFiles.push({
|
|
1996
|
+
path: join(root, "package.json"),
|
|
1997
|
+
action:
|
|
1998
|
+
JSON.stringify(scripts) === JSON.stringify(manifest.scripts)
|
|
1999
|
+
? "skip"
|
|
2000
|
+
: "write",
|
|
2001
|
+
reason: `Next.js scripts: ${
|
|
2002
|
+
Object.entries(scripts)
|
|
2003
|
+
.filter(([name, value]) => value !== originalScripts[name])
|
|
2004
|
+
.map(
|
|
2005
|
+
([name, value]) =>
|
|
2006
|
+
`${name}: ${JSON.stringify(originalScripts[name])} -> ${JSON.stringify(value)}`,
|
|
2007
|
+
)
|
|
2008
|
+
.join("; ") || "unchanged"
|
|
2009
|
+
}`,
|
|
2010
|
+
})
|
|
2011
|
+
}
|
|
1757
2012
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2013
|
+
const dependencyTargets =
|
|
2014
|
+
uiRoot === root
|
|
2015
|
+
? [{ cwd: root, ...appPending }]
|
|
2016
|
+
: [
|
|
2017
|
+
{ cwd: uiRoot, ...baseDependencies },
|
|
2018
|
+
{ cwd: root, ...appPending },
|
|
2019
|
+
]
|
|
2020
|
+
|
|
2021
|
+
for (const group of dependencyTargets) {
|
|
2022
|
+
if (!group.dependencies.length && !group.devDependencies.length) continue
|
|
2023
|
+
const path = join(group.cwd, "package.json")
|
|
2024
|
+
const planned = plannedFiles.find((file) => file.path === path)
|
|
2025
|
+
|
|
2026
|
+
if (planned) {
|
|
2027
|
+
planned.action = "write"
|
|
2028
|
+
planned.reason = [
|
|
2029
|
+
planned.reason,
|
|
2030
|
+
"Pending package-manager dependency changes",
|
|
2031
|
+
]
|
|
2032
|
+
.filter(Boolean)
|
|
2033
|
+
.join("; ")
|
|
2034
|
+
} else {
|
|
2035
|
+
plannedFiles.push({
|
|
2036
|
+
path,
|
|
2037
|
+
action: "write",
|
|
2038
|
+
reason: "Pending package-manager dependency changes",
|
|
2039
|
+
})
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1762
2042
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2043
|
+
for (const warning of basePlan.warnings) options.onWarning?.(warning)
|
|
2044
|
+
|
|
2045
|
+
return {
|
|
2046
|
+
framework,
|
|
2047
|
+
configured: 0,
|
|
2048
|
+
...(basePlan.warnings.length && { warnings: basePlan.warnings }),
|
|
2049
|
+
preview: {
|
|
2050
|
+
files: [
|
|
2051
|
+
...basePlan.preview.files.map((file) => ({
|
|
2052
|
+
...file,
|
|
2053
|
+
path: join(uiRoot, file.path),
|
|
2054
|
+
})),
|
|
2055
|
+
...plannedFiles,
|
|
2056
|
+
],
|
|
2057
|
+
dependencies: dependencyTargets,
|
|
2058
|
+
prerequisites: [
|
|
2059
|
+
"Supported framework, safe project paths and configuration",
|
|
2060
|
+
"Validated registry base with @stylex; stylesheet directive",
|
|
2061
|
+
],
|
|
2062
|
+
},
|
|
2063
|
+
}
|
|
1768
2064
|
}
|
|
1769
2065
|
|
|
1770
|
-
|
|
2066
|
+
let baseInstalled = false
|
|
1771
2067
|
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
2068
|
+
try {
|
|
2069
|
+
const baseResult = await applyInstall(baseApplicationPlan, options)
|
|
2070
|
+
baseInstalled = true
|
|
2071
|
+
|
|
2072
|
+
if (
|
|
2073
|
+
!(await readFile(join(uiRoot, "src/styles/styles.css"), "utf8")).includes(
|
|
2074
|
+
"@stylex;",
|
|
2075
|
+
)
|
|
2076
|
+
) {
|
|
2077
|
+
throw new Error(
|
|
2078
|
+
"Add @stylex; to src/styles/styles.css before configuring StyleX",
|
|
2079
|
+
)
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (!(await existingFile(root, "package.json")))
|
|
2083
|
+
throw new Error("Missing package.json after installation")
|
|
2084
|
+
const packages = await readFile(join(root, "package.json"), "utf8")
|
|
2085
|
+
const installed: JsonValue = JSON.parse(packages)
|
|
1776
2086
|
|
|
1777
|
-
if (!object(
|
|
2087
|
+
if (!object(installed))
|
|
1778
2088
|
throw new Error("Invalid package.json after installation")
|
|
1779
2089
|
|
|
1780
|
-
|
|
1781
|
-
|
|
2090
|
+
const pending = await pendingDependencies(
|
|
2091
|
+
root,
|
|
2092
|
+
runtimeRequirements,
|
|
2093
|
+
devRequirements,
|
|
2094
|
+
)
|
|
2095
|
+
|
|
2096
|
+
if (uiRoot === root) {
|
|
2097
|
+
pending.dependencies = pending.dependencies.filter(
|
|
2098
|
+
(argument) => !isBaseDependency(argument),
|
|
2099
|
+
)
|
|
2100
|
+
pending.devDependencies = pending.devDependencies.filter(
|
|
2101
|
+
(argument) => !isBaseDependency(argument),
|
|
2102
|
+
)
|
|
1782
2103
|
}
|
|
1783
2104
|
|
|
1784
|
-
|
|
2105
|
+
if (pending.dependencies.length)
|
|
2106
|
+
await packageRun(["add", ...pending.dependencies], root)
|
|
2107
|
+
|
|
2108
|
+
if (pending.devDependencies.length)
|
|
2109
|
+
await packageRun(["add", "-d", ...pending.devDependencies], root)
|
|
2110
|
+
|
|
2111
|
+
if (shared) {
|
|
2112
|
+
const path = `${relative(root, uiRoot).replaceAll("\\", "/")}/package.json`
|
|
2113
|
+
const planned = edits.get(path)
|
|
2114
|
+
|
|
2115
|
+
if (planned) {
|
|
2116
|
+
if (!(await existingFile(root, path)))
|
|
2117
|
+
throw new Error("Missing UI package.json after installation")
|
|
2118
|
+
const current = await readFile(join(uiRoot, "package.json"), "utf8")
|
|
2119
|
+
const before: JsonValue = JSON.parse(planned.before!)
|
|
2120
|
+
const after: JsonValue = JSON.parse(planned.after)
|
|
2121
|
+
const value: JsonValue = JSON.parse(current)
|
|
2122
|
+
|
|
2123
|
+
if (
|
|
2124
|
+
!object(before) ||
|
|
2125
|
+
!object(after) ||
|
|
2126
|
+
!object(value) ||
|
|
2127
|
+
value.name !== before.name ||
|
|
2128
|
+
JSON.stringify(value.exports) !== JSON.stringify(before.exports) ||
|
|
2129
|
+
JSON.stringify(value.sideEffects) !==
|
|
2130
|
+
JSON.stringify(before.sideEffects)
|
|
2131
|
+
)
|
|
2132
|
+
throw new Error("UI package exports changed during installation")
|
|
1785
2133
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2134
|
+
edits.set(path, {
|
|
2135
|
+
before: current,
|
|
2136
|
+
after: `${JSON.stringify({ ...value, exports: after.exports, sideEffects: after.sideEffects }, null, 2)}\n`,
|
|
2137
|
+
})
|
|
2138
|
+
}
|
|
1791
2139
|
}
|
|
1792
|
-
}
|
|
1793
2140
|
|
|
1794
|
-
|
|
2141
|
+
const changes = new Map<string, FileChange>(edits)
|
|
2142
|
+
|
|
2143
|
+
for (const [path, before] of retired) {
|
|
2144
|
+
changes.set(path, { before, after: null })
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
if (framework === "next") {
|
|
2148
|
+
if (!(await existingFile(root, "package.json")))
|
|
2149
|
+
throw new Error("Missing package.json after installation")
|
|
2150
|
+
const before = await readFile(join(root, "package.json"), "utf8")
|
|
2151
|
+
const latest: JsonValue = JSON.parse(before)
|
|
2152
|
+
|
|
2153
|
+
if (!object(latest))
|
|
2154
|
+
throw new Error("Invalid package.json after installation")
|
|
2155
|
+
|
|
2156
|
+
if (JSON.stringify(latest.scripts) !== JSON.stringify(manifest.scripts)) {
|
|
2157
|
+
throw new Error("Next.js scripts changed during init")
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
const scripts = nextScripts(latest.scripts)
|
|
2161
|
+
|
|
2162
|
+
changes.set("package.json", {
|
|
2163
|
+
before,
|
|
2164
|
+
after:
|
|
2165
|
+
JSON.stringify(latest.scripts) === JSON.stringify(scripts)
|
|
2166
|
+
? before
|
|
2167
|
+
: `${JSON.stringify({ ...latest, scripts }, null, 2)}\n`,
|
|
2168
|
+
})
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
await writeFiles(root, changes)
|
|
2172
|
+
|
|
2173
|
+
return {
|
|
2174
|
+
framework,
|
|
2175
|
+
configured: edits.size,
|
|
2176
|
+
...(baseResult.warnings && { warnings: baseResult.warnings }),
|
|
2177
|
+
}
|
|
2178
|
+
} catch (cause) {
|
|
2179
|
+
if (!baseInstalled) throw cause
|
|
2180
|
+
throw new Error(
|
|
2181
|
+
`${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.`,
|
|
2182
|
+
{ cause },
|
|
2183
|
+
)
|
|
2184
|
+
}
|
|
1795
2185
|
}
|