@evertondgn/solid-tooling 0.2.0 → 0.4.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.
@@ -0,0 +1 @@
1
+ export default function checkServerLog(): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { env } from "node:process";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ // src/playwright/teardown.ts
5
+ const PRIVATE_MARKER = /PRIVATE_[A-Z0-9_]+/u;
6
+ async function checkLog({ file, baseUrl }) {
7
+ if (!file || !baseUrl) throw new Error("Server log file is not configured");
8
+ const log = stripVTControlCharacters(await readFile(file, "utf8"));
9
+ const { host } = new URL(baseUrl);
10
+ if (!log.includes(host)) throw new Error(`Server log does not show the preview startup: ${file}`);
11
+ if (PRIVATE_MARKER.test(log)) throw new Error(`Server log contains a private marker: ${file}`);
12
+ }
13
+ async function checkServerLog() {
14
+ await checkLog({
15
+ file: env.SERVER_LOG_FILE,
16
+ baseUrl: env.BASE_URL_TEST
17
+ });
18
+ if (env.TEST_PUBLICATION_LOAD === "true") await checkLog({
19
+ file: env.SERVER_LOG_SECONDARY,
20
+ baseUrl: env.PUBLICATION_SECONDARY_URL
21
+ });
22
+ }
23
+ export { checkServerLog as default };
@@ -0,0 +1,18 @@
1
+ import { PlaywrightTestConfig } from "@playwright/test";
2
+ type FixtureServer = {
3
+ command: string;
4
+ url: string;
5
+ };
6
+ type SerialSuites = {
7
+ serverLogs?: string;
8
+ markdown?: string;
9
+ publicationLoad?: string;
10
+ };
11
+ type PlaywrightOptions = {
12
+ root: string;
13
+ testDir?: string;
14
+ fixtureServers?: FixtureServer[];
15
+ suites?: SerialSuites;
16
+ };
17
+ export declare function definePlaywrightConfig({ root, testDir, fixtureServers, suites }: PlaywrightOptions): PlaywrightTestConfig;
18
+ export {};
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { env, loadEnvFile } from "node:process";
4
+ import { loadEnv } from "vite-plus";
5
+ import { defineConfig as defineConfig$1, devices } from "@playwright/test";
6
+ // src/playwright/index.ts
7
+ const STARTUP_TIMEOUT = 12e4;
8
+ const SECONDARY_PORT_OFFSET = 2;
9
+ const MAX_PORT = 65535;
10
+ const CI_WORKERS = 2;
11
+ const SECONDARY_LOG = "test-results/server-publication-secondary.log";
12
+ const PREBUILT_GUARD = "if [ -e .vercel/output/static/.vite ]; then echo 'TEST_PREBUILT found an e2e build; run vp run build first' >&2; exit 1; fi && ";
13
+ function readRunMode(suites) {
14
+ const mode = {
15
+ isProduction: env.TEST_PRODUCTION_ROUTES === "true",
16
+ isPublicationLoad: env.TEST_PUBLICATION_LOAD === "true",
17
+ shouldBuild: env.TEST_PREBUILT !== "true"
18
+ };
19
+ if (mode.isPublicationLoad && mode.isProduction) throw new Error("TEST_PUBLICATION_LOAD requires fixture routes");
20
+ if (mode.isPublicationLoad && !suites.publicationLoad) throw new Error("TEST_PUBLICATION_LOAD requires a publicationLoad suite");
21
+ if (!mode.shouldBuild && !mode.isProduction) throw new Error("TEST_PREBUILT requires production routes");
22
+ return mode;
23
+ }
24
+ function readBaseUrl(mode) {
25
+ if (!env.BASE_URL_TEST) throw new Error(".env.test must define BASE_URL_TEST");
26
+ const baseUrl = new URL(env.BASE_URL_TEST);
27
+ if (!baseUrl.port) throw new Error("BASE_URL_TEST must include an explicit port");
28
+ if (mode.isPublicationLoad && Number(baseUrl.port) + SECONDARY_PORT_OFFSET > MAX_PORT) throw new Error("BASE_URL_TEST leaves no port for the secondary server");
29
+ return env.BASE_URL_TEST;
30
+ }
31
+ function readWorkers(mode) {
32
+ if (mode.isPublicationLoad) return 1;
33
+ return env.CI ? CI_WORKERS : void 0;
34
+ }
35
+ function secondaryUrl(baseUrl) {
36
+ const url = new URL(baseUrl);
37
+ url.port = String(Number(url.port) + SECONDARY_PORT_OFFSET);
38
+ return url;
39
+ }
40
+ function buildProjects({ mode, suites }) {
41
+ if (mode.isPublicationLoad) return [{
42
+ name: "publication-load",
43
+ testMatch: suites.publicationLoad
44
+ }];
45
+ if (mode.isProduction) return [{ name: "chromium" }];
46
+ const teardown = suites.serverLogs ? { teardown: "server-logs" } : {};
47
+ const projects = [{
48
+ name: "chromium",
49
+ testIgnore: ["**/*.production.e2e.test.ts", ...[
50
+ suites.serverLogs,
51
+ suites.publicationLoad,
52
+ suites.markdown
53
+ ].filter((suite) => suite !== void 0)],
54
+ ...teardown
55
+ }];
56
+ if (suites.markdown) projects.push({
57
+ name: "markdown",
58
+ testMatch: suites.markdown,
59
+ fullyParallel: false,
60
+ workers: 1,
61
+ ...teardown
62
+ });
63
+ if (suites.serverLogs) projects.push({
64
+ name: "server-logs",
65
+ testMatch: suites.serverLogs,
66
+ fullyParallel: false
67
+ });
68
+ return projects;
69
+ }
70
+ function buildServers({ root, mode, fixtureServers, baseUrl, serverLog }) {
71
+ const buildMode = mode.isProduction ? "production" : "e2e";
72
+ const build = mode.shouldBuild ? `vp run build --mode ${buildMode} && ` : PREBUILT_GUARD;
73
+ const shared = {
74
+ cwd: root,
75
+ reuseExistingServer: false
76
+ };
77
+ const server = {
78
+ ...shared,
79
+ timeout: STARTUP_TIMEOUT
80
+ };
81
+ const fixtures = mode.isProduction ? [] : fixtureServers.map((fixture) => ({
82
+ ...fixture,
83
+ ...shared
84
+ }));
85
+ const { hostname, port } = new URL(baseUrl);
86
+ const secondary = secondaryUrl(baseUrl);
87
+ return [
88
+ ...fixtures,
89
+ {
90
+ ...server,
91
+ command: `${build}mkdir -p test-results && vp run start --strictPort > ${serverLog} 2>&1`,
92
+ env: {
93
+ HOST: hostname,
94
+ PORT: port,
95
+ NODE_ENV: "production"
96
+ },
97
+ url: baseUrl
98
+ },
99
+ ...mode.isPublicationLoad ? [{
100
+ ...server,
101
+ command: `vp run start --strictPort > ${SECONDARY_LOG} 2>&1`,
102
+ env: {
103
+ HOST: hostname,
104
+ PORT: secondary.port,
105
+ NODE_ENV: "production"
106
+ },
107
+ url: secondary.href
108
+ }] : []
109
+ ];
110
+ }
111
+ function definePlaywrightConfig({ root, testDir = "src/tests/pages", fixtureServers = [], suites = {} }) {
112
+ loadEnvFile(path.join(root, ".env.test"));
113
+ const mode = readRunMode(suites);
114
+ const buildMode = mode.isProduction ? "production" : "e2e";
115
+ Object.assign(env, loadEnv(buildMode, root, "VITE_"));
116
+ const baseUrl = readBaseUrl(mode);
117
+ const serverLog = `test-results/server-${buildMode}.log`;
118
+ env.SERVER_LOG_FILE = path.join(root, serverLog);
119
+ if (mode.isPublicationLoad) {
120
+ env.PUBLICATION_SECONDARY_URL = secondaryUrl(baseUrl).href;
121
+ env.SERVER_LOG_SECONDARY = path.join(root, SECONDARY_LOG);
122
+ }
123
+ const reportFolder = mode.isProduction ? "production" : "e2e";
124
+ return defineConfig$1({
125
+ testDir: path.join(root, testDir),
126
+ testMatch: mode.isProduction ? "**/*.production.e2e.test.ts" : "**/*.e2e.test.ts",
127
+ outputDir: path.join(root, mode.isProduction ? "test-results/e2e-production" : "test-results/e2e"),
128
+ globalTeardown: fileURLToPath(new URL("playwright-teardown.mjs", import.meta.url)),
129
+ fullyParallel: !mode.isPublicationLoad,
130
+ forbidOnly: true,
131
+ workers: readWorkers(mode),
132
+ reporter: [["list"], ["html", {
133
+ outputFolder: path.join(root, "playwright-report", reportFolder),
134
+ open: "never",
135
+ onlyFailures: Boolean(env.CI)
136
+ }]],
137
+ use: {
138
+ ...devices["Desktop Chrome"],
139
+ channel: env.CI ? "chrome" : void 0,
140
+ baseURL: baseUrl,
141
+ locale: "en-US",
142
+ screenshot: "only-on-failure",
143
+ trace: "retain-on-failure"
144
+ },
145
+ projects: buildProjects({
146
+ mode,
147
+ suites
148
+ }),
149
+ webServer: buildServers({
150
+ root,
151
+ mode,
152
+ fixtureServers,
153
+ baseUrl,
154
+ serverLog
155
+ })
156
+ });
157
+ }
158
+ export { definePlaywrightConfig };
@@ -0,0 +1,8 @@
1
+ import { Config } from "stylelint";
2
+ type CssCheck = {
3
+ report: string;
4
+ hasErrors: boolean;
5
+ };
6
+ export declare function defineStylelintConfig(appRoot: string): Promise<Config>;
7
+ export declare function checkCss(appRoot: string): Promise<CssCheck>;
8
+ export {};
@@ -0,0 +1,180 @@
1
+ import { cssFilenameError } from "./css-filename.mjs";
2
+ import { t as hasMatch } from "./has-match-BITlj0EE.mjs";
3
+ import path from "node:path";
4
+ import { readdir } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import stylelint from "stylelint";
7
+ // src/stylelint/index.ts
8
+ const VAR = String.raw`var\(\s*--[a-z0-9-]+\s*\)`;
9
+ const TOKEN = String.raw`/^${VAR}(?:\s*[,/]\s*${VAR}|\s+${VAR})*$/`;
10
+ const LINE_HEIGHT = String.raw`(?:\d*\.?\d+(?:%|[a-z]+)?|normal)`;
11
+ const FONT_TOKEN = String.raw`/^${VAR}(?:\s+${VAR})*(?:\s*\/\s*(?:${LINE_HEIGHT}|${VAR})(?:\s+${VAR})*)?$/`;
12
+ const TOKEN_KEYWORDS = "/^(?:transparent|currentcolor|inherit|initial|unset|revert|revert-layer|none|auto)$/i";
13
+ const TOKEN_FAMILIES = [
14
+ "color",
15
+ "gradient",
16
+ "radius",
17
+ "shadow",
18
+ "ring",
19
+ "font-size",
20
+ "font-weight",
21
+ "z-index"
22
+ ];
23
+ const REFERENCE_FILES = ["src/theme/tokens/**/*.css", "src/theme/class/animation.css"];
24
+ const SYSTEM_COLORS = `
25
+ AccentColor AccentColorText ActiveText ButtonBorder ButtonFace ButtonText
26
+ Canvas CanvasText Field FieldText GrayText Highlight HighlightText LinkText
27
+ Mark MarkText SelectedItem SelectedItemText VisitedText
28
+ ActiveBorder ActiveCaption AppWorkspace Background ButtonHighlight
29
+ ButtonShadow CaptionText InactiveBorder InactiveCaption InactiveCaptionText
30
+ InfoBackground InfoText Menu MenuText Scrollbar ThreeDDarkShadow ThreeDFace
31
+ ThreeDHighlight ThreeDLightShadow ThreeDShadow Window WindowFrame WindowText
32
+ -webkit-activelink -webkit-focus-ring-color -webkit-link -webkit-text
33
+ `.trim().split(/\s+/u);
34
+ const SYSTEM_COLOR = String.raw`/(?<![\w#./-])(?:${SYSTEM_COLORS.join("|")})(?![\w.-])/i`;
35
+ const NAME_PROPERTIES = String.raw`animation(?:-name)?|transition(?:-property)?|will-change|grid(?:-[a-z]+)*|container(?:-name)?|counter-(?:increment|reset|set)|view-transition-(?:class|name)|font-family|content|quotes|list-style(?:-type)?|page`;
36
+ const CSS_FILES = "src/**/*.css";
37
+ function property(names) {
38
+ return String.raw`/^(?!--)(?:-[a-z]+-)?(?:${names})$/i`;
39
+ }
40
+ const TOKEN_PROPERTIES = [
41
+ [property("(?:[a-z]+-)*color|fill|stroke"), ["/^--color-/"]],
42
+ [property("(?:[a-z]+-)*radius"), ["/^--radius-/"]],
43
+ [property("background"), ["/^--(?:color|gradient)-/"]],
44
+ [property("background-image"), ["/^--gradient-/"]],
45
+ [property("box-shadow"), ["/^--(?:shadow|ring)-/"]],
46
+ [property("text-shadow"), []],
47
+ [
48
+ property("font"),
49
+ ["/^--font-/"],
50
+ FONT_TOKEN
51
+ ],
52
+ [property("font-size"), ["/^--font-size-/"]],
53
+ [property("font-weight"), ["/^--font-weight-/"]],
54
+ [property("z-index"), ["/^--z-index-/"]],
55
+ ...TOKEN_FAMILIES.map((family) => [`/^--${family}-/`, [`/^--${family}-/`]]),
56
+ [
57
+ "/^--font-(?!size-|weight-)/",
58
+ ["/^--font-/"],
59
+ FONT_TOKEN
60
+ ]
61
+ ];
62
+ async function referenceFiles(root) {
63
+ return (await Promise.all(REFERENCE_FILES.map(async (pattern) => await hasMatch({
64
+ root,
65
+ pattern
66
+ }) ? [path.join(root, pattern)] : []))).flat();
67
+ }
68
+ async function filenameDiagnostics(root) {
69
+ return (await readdir(path.join(root, "src"), {
70
+ recursive: true,
71
+ withFileTypes: true
72
+ })).flatMap((entry) => {
73
+ if (!entry.isFile()) return [];
74
+ const absolute = path.join(entry.parentPath, entry.name);
75
+ const file = path.relative(root, absolute).split(path.sep).join("/");
76
+ if (!file.endsWith(".css")) return [];
77
+ const message = cssFilenameError(file);
78
+ return message ? [{
79
+ file,
80
+ message
81
+ }] : [];
82
+ });
83
+ }
84
+ async function defineStylelintConfig(appRoot) {
85
+ const root = path.resolve(appRoot);
86
+ return {
87
+ referenceFiles: await referenceFiles(root),
88
+ reportDescriptionlessDisables: true,
89
+ plugins: [fileURLToPath(import.meta.resolve("stylelint-use-nesting"))],
90
+ rules: {
91
+ "property-no-unknown": true,
92
+ "declaration-property-value-no-unknown": true,
93
+ "value-no-invalid": true,
94
+ "unit-no-unknown": true,
95
+ "color-no-invalid-hex": true,
96
+ "color-no-hex": true,
97
+ "color-named": "never",
98
+ "declaration-block-no-duplicate-properties": [true, { ignore: ["consecutive-duplicates-with-different-syntaxes"] }],
99
+ "declaration-block-no-shorthand-property-overrides": true,
100
+ "selector-pseudo-class-no-unknown": true,
101
+ "selector-pseudo-element-no-unknown": true,
102
+ "selector-type-no-unknown": true,
103
+ "at-rule-no-unknown": true,
104
+ "no-unknown-animations": true,
105
+ "no-unknown-custom-properties": true,
106
+ "custom-property-no-missing-var-function": true,
107
+ "custom-property-pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
108
+ "selector-no-invalid": true,
109
+ "selector-no-deprecated": true,
110
+ "selector-no-unmatchable": true,
111
+ "at-charset-rule-no-invalid": true,
112
+ "media-query-no-invalid": true,
113
+ "media-feature-name-no-unknown": true,
114
+ "media-feature-name-value-no-unknown": true,
115
+ "function-calc-no-unspaced-operator": true,
116
+ "no-invalid-position-at-import-rule": true,
117
+ "no-duplicate-selectors": true,
118
+ "keyframe-block-no-duplicate-selectors": true,
119
+ "function-disallowed-list": [
120
+ "rgb",
121
+ "rgba",
122
+ "hsl",
123
+ "hsla",
124
+ "hwb",
125
+ "lab",
126
+ "lch",
127
+ "oklab",
128
+ "oklch",
129
+ "color",
130
+ "color-mix",
131
+ "light-dark",
132
+ "contrast-color",
133
+ "device-cmyk"
134
+ ],
135
+ "value-type-custom-property-required-list": [["color"], { ignoreValues: [TOKEN_KEYWORDS] }],
136
+ "declaration-property-value-allowed-list": [Object.fromEntries(TOKEN_PROPERTIES.map(([name, , value = TOKEN]) => [name, [value, TOKEN_KEYWORDS]])), { message: "Use only var() tokens in this property; an exception needs a stylelint-disable comment with the reason." }],
137
+ "declaration-property-custom-property-allowed-list": [Object.fromEntries(TOKEN_PROPERTIES.map(([name, families]) => [name, families])), { message: "The property \"%s\" does not accept the token \"%s\"." }],
138
+ "declaration-property-value-disallowed-list": [{ [String.raw`/^(?!(?:-[a-z]+-)?(?:${NAME_PROPERTIES})$)/i`]: [SYSTEM_COLOR] }, { message: "System colors belong in src/theme/." }],
139
+ "selector-class-pattern": ["^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", { message: "Use snake_case for CSS classes, as in variant_default." }],
140
+ "max-nesting-depth": 2,
141
+ "nesting-selector-no-missing-scoping-root": true,
142
+ "csstools/use-nesting": "always"
143
+ },
144
+ overrides: [{
145
+ files: [path.join(root, "src/theme/**/*.css")],
146
+ rules: {
147
+ "function-disallowed-list": [
148
+ "rgb",
149
+ "rgba",
150
+ "hsl",
151
+ "hsla",
152
+ "hwb",
153
+ "lab",
154
+ "lch",
155
+ "color"
156
+ ],
157
+ "value-type-custom-property-required-list": null,
158
+ "declaration-property-value-allowed-list": null,
159
+ "declaration-property-custom-property-allowed-list": null,
160
+ "declaration-property-value-disallowed-list": null
161
+ }
162
+ }]
163
+ };
164
+ }
165
+ async function checkCss(appRoot) {
166
+ const root = path.resolve(appRoot);
167
+ const config = await defineStylelintConfig(root);
168
+ const [filenames, result] = await Promise.all([filenameDiagnostics(root), stylelint.lint({
169
+ cwd: root,
170
+ files: [CSS_FILES],
171
+ config,
172
+ formatter: "string"
173
+ })]);
174
+ const filenameReport = filenames.map(({ file, message }) => `${file}:1 error project/css-filename: ${message}\n`).join("");
175
+ return {
176
+ report: result.report + filenameReport,
177
+ hasErrors: result.errored || filenames.length > 0
178
+ };
179
+ }
180
+ export { checkCss, defineStylelintConfig };
@@ -0,0 +1,40 @@
1
+ import { PluginOption, UserConfig, ViteUserConfigFnObject } from "vite-plus";
2
+ import { NitroConfig, NitroModuleInput, PublicAssetDir } from "nitro/types";
3
+ type StaticSecurityHeaders = Record<'strict-transport-security' | 'x-content-type-options' | 'cross-origin-resource-policy', string>;
4
+ type E2eOptions = {
5
+ middleware?: string;
6
+ routes?: string;
7
+ };
8
+ type WorkspaceOptions = {
9
+ dependsOn: string[];
10
+ inputs: string[];
11
+ };
12
+ type StaticAssetDir = PublicAssetDir & {
13
+ baseURL: string;
14
+ };
15
+ type RouteRule = NonNullable<NitroConfig['routeRules']>[string];
16
+ type RunTask = NonNullable<NonNullable<UserConfig['run']>['tasks']>[string];
17
+ type ExtensionContext = {
18
+ target: 'app' | 'test';
19
+ mode: string;
20
+ };
21
+ type SolidAppExtension = {
22
+ plugins?: PluginOption[];
23
+ nitroModules?: NitroModuleInput[];
24
+ };
25
+ type SolidAppOptions = {
26
+ root: string;
27
+ middleware?: string;
28
+ serverSetup?: string;
29
+ e2e?: E2eOptions;
30
+ icons?: Record<string, string>;
31
+ securityHeaders: StaticSecurityHeaders;
32
+ publicAssets?: StaticAssetDir[];
33
+ staticPaths?: string[];
34
+ routeRules?: Record<string, RouteRule>;
35
+ extend?: (context: ExtensionContext) => SolidAppExtension | Promise<SolidAppExtension>;
36
+ workspace?: WorkspaceOptions;
37
+ tasks?: Record<string, RunTask>;
38
+ };
39
+ export declare function defineSolidApp(options: SolidAppOptions): ViteUserConfigFnObject;
40
+ export type { SolidAppOptions };