@roblawn/devtool-runtime 0.1.0-alpha.0 → 0.1.0-alpha.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/LICENSE.md +11 -11
- package/README.md +7 -7
- package/dist/content/envelope.d.ts +54 -0
- package/dist/content/envelope.js +10 -0
- package/dist/content/envelopeResolver.d.ts +14 -0
- package/dist/content/envelopeResolver.js +77 -0
- package/dist/content/identity.d.ts +16 -0
- package/dist/content/identity.js +1 -0
- package/dist/content/index.d.ts +10 -0
- package/dist/content/index.js +10 -0
- package/dist/content/normalize.d.ts +8 -0
- package/dist/content/normalize.js +53 -0
- package/dist/content/profile.d.ts +11 -0
- package/dist/content/profile.js +29 -0
- package/dist/content/provider.d.ts +17 -0
- package/dist/content/provider.js +1 -0
- package/dist/content/safeContent.d.ts +2 -0
- package/dist/content/safeContent.js +44 -0
- package/dist/content/scope.d.ts +6 -0
- package/dist/content/scope.js +41 -0
- package/dist/content/slotItems.d.ts +13 -0
- package/dist/content/slotItems.js +53 -0
- package/dist/content/targets.d.ts +24 -0
- package/dist/content/targets.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schema/LayoutDefinitionMeta.d.ts +6 -0
- package/dist/shared/legacyNames.d.ts +6 -0
- package/dist/shared/legacyNames.js +8 -0
- package/dist/static/buildSsg.d.ts +11 -0
- package/dist/static/buildSsg.js +93 -0
- package/dist/static/index.d.ts +4 -0
- package/dist/static/index.js +4 -0
- package/dist/static/packageNetlify.d.ts +28 -0
- package/dist/static/packageNetlify.js +141 -0
- package/dist/static/packageStaticSite.d.ts +19 -0
- package/dist/static/packageStaticSite.js +144 -0
- package/dist/static/serveStaticSite.d.ts +9 -0
- package/dist/static/serveStaticSite.js +143 -0
- package/dist/theme-contract/ops/coreOps.js +5 -47
- package/dist/theme-contract/ops/emitters.d.ts +2 -0
- package/dist/theme-contract/ops/emitters.js +38 -2
- package/dist/theme-contract/tokens/schemas/spacing.token.schema.d.ts +9 -2
- package/dist/theme-contract/tokens/schemas/spacing.token.schema.js +9 -2
- package/dist/theme-contract/tokens/tokens.schema.d.ts +9 -2
- package/dist/vite/embeddedJsonPlugin.d.ts +8 -0
- package/dist/vite/embeddedJsonPlugin.js +109 -0
- package/dist/vite/index.d.ts +4 -0
- package/dist/vite/index.js +4 -0
- package/dist/vite/localJsonPlugin.d.ts +4 -0
- package/dist/vite/localJsonPlugin.js +70 -0
- package/dist/vite/ssgEntryPlugin.d.ts +7 -0
- package/dist/vite/ssgEntryPlugin.js +21 -0
- package/dist/vite/staticFallbackPlugin.d.ts +8 -0
- package/dist/vite/staticFallbackPlugin.js +77 -0
- package/package.json +47 -27
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const DEFAULT_ARGS = {
|
|
6
|
+
configFile: 'vite.config.ssg.ts',
|
|
7
|
+
mode: 'ssg',
|
|
8
|
+
outDir: 'ssg-dist',
|
|
9
|
+
};
|
|
10
|
+
const VITE_SSG_NODE_MODULE = 'vite-ssg/node';
|
|
11
|
+
export function parseStaticSsgBuildArgs(argv) {
|
|
12
|
+
const args = { ...DEFAULT_ARGS };
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const arg = argv[index];
|
|
15
|
+
if (arg === '--config' || arg === '-c') {
|
|
16
|
+
args.configFile = argv[index + 1] ?? args.configFile;
|
|
17
|
+
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (arg.startsWith('--config=')) {
|
|
21
|
+
args.configFile = arg.slice('--config='.length);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg === '--mode') {
|
|
25
|
+
args.mode = argv[index + 1] ?? args.mode;
|
|
26
|
+
index += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg.startsWith('--mode=')) {
|
|
30
|
+
args.mode = arg.slice('--mode='.length);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === '--outDir') {
|
|
34
|
+
args.outDir = argv[index + 1] ?? args.outDir;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg.startsWith('--outDir=')) {
|
|
39
|
+
args.outDir = arg.slice('--outDir='.length);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
export async function buildStaticSsg(args = {}, options = {}) {
|
|
45
|
+
const buildArgs = { ...DEFAULT_ARGS, ...args };
|
|
46
|
+
const cwd = options.cwd ?? process.cwd();
|
|
47
|
+
try {
|
|
48
|
+
const { build } = await importViteSsgNode(cwd);
|
|
49
|
+
await build({ mode: buildArgs.mode }, { configFile: buildArgs.configFile });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isWindowsTempCleanupError(error) && (await renderedOutputExists(cwd, buildArgs.outDir))) {
|
|
54
|
+
console.warn('[build:ssg] vite-ssg rendered output, but Windows could not remove .vite-ssg-temp; continuing.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function importViteSsgNode(projectRoot) {
|
|
61
|
+
const projectRequire = createRequire(path.resolve(projectRoot, 'package.json'));
|
|
62
|
+
const resolved = projectRequire.resolve(VITE_SSG_NODE_MODULE);
|
|
63
|
+
return (await import(pathToFileURL(resolved).href));
|
|
64
|
+
}
|
|
65
|
+
export async function runStaticSsgBuildCli(argv = process.argv.slice(2)) {
|
|
66
|
+
await buildStaticSsg(parseStaticSsgBuildArgs(argv));
|
|
67
|
+
}
|
|
68
|
+
function isWindowsTempCleanupError(error) {
|
|
69
|
+
if (!(error instanceof Error))
|
|
70
|
+
return false;
|
|
71
|
+
const withNodeFields = error;
|
|
72
|
+
return (withNodeFields.code === 'EBUSY' &&
|
|
73
|
+
withNodeFields.syscall === 'unlink' &&
|
|
74
|
+
typeof withNodeFields.path === 'string' &&
|
|
75
|
+
withNodeFields.path.includes(`${path.sep}.vite-ssg-temp${path.sep}`));
|
|
76
|
+
}
|
|
77
|
+
async function renderedOutputExists(projectRoot, outDir) {
|
|
78
|
+
const checks = [
|
|
79
|
+
{ file: path.resolve(projectRoot, outDir, 'index.html'), minBytes: 1024 },
|
|
80
|
+
{ file: path.resolve(projectRoot, outDir, '404.html'), minBytes: 256 },
|
|
81
|
+
];
|
|
82
|
+
for (const check of checks) {
|
|
83
|
+
try {
|
|
84
|
+
const stat = await fs.stat(check.file);
|
|
85
|
+
if (!stat.isFile() || stat.size < check.minBytes)
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type StaticSiteManifest = {
|
|
2
|
+
contract: string;
|
|
3
|
+
deployDir: string;
|
|
4
|
+
outputMode?: 'json-spa' | 'ssg';
|
|
5
|
+
pages: string[];
|
|
6
|
+
contentAssets: string[];
|
|
7
|
+
totalFiles: number;
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
};
|
|
10
|
+
export type NetlifyPlanManifest = {
|
|
11
|
+
contract: string;
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
deployDir: string;
|
|
15
|
+
shareProvider: string;
|
|
16
|
+
siteIdConfigured: boolean;
|
|
17
|
+
authTokenConfigured: boolean;
|
|
18
|
+
passwordConfigured: boolean;
|
|
19
|
+
files: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
bytes: number;
|
|
22
|
+
}>;
|
|
23
|
+
totalFiles: number;
|
|
24
|
+
totalBytes: number;
|
|
25
|
+
staticManifest: StaticSiteManifest;
|
|
26
|
+
};
|
|
27
|
+
export declare function packageNetlify(argv?: string[]): Promise<NetlifyPlanManifest>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { legacyContract, LEGACY_NAME_LOWER, LEGACY_PRIVATE_DIR, LEGACY_PUBLIC_DIR, } from '../shared/legacyNames.js';
|
|
4
|
+
const REQUIRED_STATIC_CONTRACT = legacyContract('static-site-package.v1');
|
|
5
|
+
const ENV_PREFIX = 'TUN' + 'DRA';
|
|
6
|
+
const SHARE_PROVIDER_ENV = `${ENV_PREFIX}_SHARE_PROVIDER`;
|
|
7
|
+
const SHARE_PASSWORD_ENV = `${ENV_PREFIX}_SHARE_PASSWORD`;
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = { check: false, dryRun: true };
|
|
10
|
+
for (const arg of argv) {
|
|
11
|
+
if (arg === '--check')
|
|
12
|
+
args.check = true;
|
|
13
|
+
if (arg === '--dry-run')
|
|
14
|
+
args.dryRun = true;
|
|
15
|
+
}
|
|
16
|
+
return args;
|
|
17
|
+
}
|
|
18
|
+
async function fileExists(abs) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await fs.stat(abs);
|
|
21
|
+
return stat.isFile();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function listFiles(dir) {
|
|
28
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name === '.vite')
|
|
32
|
+
continue;
|
|
33
|
+
const abs = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
files.push(...(await listFiles(abs)));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
files.push(abs);
|
|
39
|
+
}
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|
|
42
|
+
function toPosixRelative(root, abs) {
|
|
43
|
+
return path.relative(root, abs).replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
async function readStaticManifest(projectRoot) {
|
|
46
|
+
const manifestPath = path.resolve(projectRoot, LEGACY_PRIVATE_DIR, 'distribution', 'static-site-manifest.json');
|
|
47
|
+
const parsed = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
48
|
+
if (parsed.contract !== REQUIRED_STATIC_CONTRACT) {
|
|
49
|
+
throw new Error(`[netlify-package] Unsupported static manifest contract: ${String(parsed.contract)}`);
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
async function readEnvFile(projectRoot) {
|
|
54
|
+
const envPath = path.resolve(projectRoot, ['.env', LEGACY_NAME_LOWER].join('.'));
|
|
55
|
+
if (!(await fileExists(envPath)))
|
|
56
|
+
return {};
|
|
57
|
+
const env = {};
|
|
58
|
+
const source = await fs.readFile(envPath, 'utf8');
|
|
59
|
+
for (const rawLine of source.split(/\r?\n/u)) {
|
|
60
|
+
const line = rawLine.trim();
|
|
61
|
+
if (!line || line.startsWith('#'))
|
|
62
|
+
continue;
|
|
63
|
+
const equals = line.indexOf('=');
|
|
64
|
+
if (equals <= 0)
|
|
65
|
+
continue;
|
|
66
|
+
const key = line.slice(0, equals).trim();
|
|
67
|
+
const value = line.slice(equals + 1).trim().replace(/^["']|["']$/g, '');
|
|
68
|
+
env[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
72
|
+
function readEnvValue(fileEnv, key) {
|
|
73
|
+
return process.env[key] ?? fileEnv[key] ?? '';
|
|
74
|
+
}
|
|
75
|
+
async function writeNetlifyStaticFiles(deployRoot, outputMode, password) {
|
|
76
|
+
const redirects = outputMode === 'ssg'
|
|
77
|
+
? ['# SSG fallback', '/* /404.html 404', '']
|
|
78
|
+
: ['# Static SPA fallback', '/* /index.html 200', ''];
|
|
79
|
+
const headers = [
|
|
80
|
+
'/assets/*',
|
|
81
|
+
' Cache-Control: public, max-age=31536000, immutable',
|
|
82
|
+
`/${LEGACY_PUBLIC_DIR}/*`,
|
|
83
|
+
' Cache-Control: public, max-age=60',
|
|
84
|
+
'',
|
|
85
|
+
];
|
|
86
|
+
if (password) {
|
|
87
|
+
if (/\s/u.test(password)) {
|
|
88
|
+
throw new Error('[netlify-package] Preview password cannot contain whitespace when using Netlify Basic-Auth.');
|
|
89
|
+
}
|
|
90
|
+
headers.push('/*', ` Basic-Auth: preview:${password}`, '');
|
|
91
|
+
}
|
|
92
|
+
await fs.writeFile(path.join(deployRoot, '_redirects'), redirects.join('\n'), 'utf8');
|
|
93
|
+
await fs.writeFile(path.join(deployRoot, '_headers'), headers.join('\n'), 'utf8');
|
|
94
|
+
}
|
|
95
|
+
async function buildNetlifyPlan(projectRoot, args) {
|
|
96
|
+
const staticManifest = await readStaticManifest(projectRoot);
|
|
97
|
+
const deployRoot = path.resolve(projectRoot, staticManifest.deployDir);
|
|
98
|
+
if (!(await fileExists(path.join(deployRoot, 'index.html')))) {
|
|
99
|
+
throw new Error('[netlify-package] Deploy directory is missing index.html. Run package:static first.');
|
|
100
|
+
}
|
|
101
|
+
const fileEnv = await readEnvFile(projectRoot);
|
|
102
|
+
const shareProvider = readEnvValue(fileEnv, SHARE_PROVIDER_ENV) || 'netlify';
|
|
103
|
+
const authToken = readEnvValue(fileEnv, 'NETLIFY_AUTH_TOKEN');
|
|
104
|
+
const siteId = readEnvValue(fileEnv, 'NETLIFY_SITE_ID');
|
|
105
|
+
const password = readEnvValue(fileEnv, SHARE_PASSWORD_ENV);
|
|
106
|
+
if (!args.check) {
|
|
107
|
+
await writeNetlifyStaticFiles(deployRoot, staticManifest.outputMode, password);
|
|
108
|
+
}
|
|
109
|
+
const files = await listFiles(deployRoot);
|
|
110
|
+
const fileEntries = await Promise.all(files.map(async (filePath) => ({
|
|
111
|
+
path: toPosixRelative(deployRoot, filePath),
|
|
112
|
+
bytes: (await fs.stat(filePath)).size,
|
|
113
|
+
})));
|
|
114
|
+
const totalBytes = fileEntries.reduce((sum, file) => sum + file.bytes, 0);
|
|
115
|
+
return {
|
|
116
|
+
contract: legacyContract('netlify-deploy-plan.v1'),
|
|
117
|
+
generatedAt: new Date().toISOString(),
|
|
118
|
+
dryRun: args.dryRun,
|
|
119
|
+
deployDir: staticManifest.deployDir,
|
|
120
|
+
shareProvider,
|
|
121
|
+
siteIdConfigured: siteId.length > 0,
|
|
122
|
+
authTokenConfigured: authToken.length > 0,
|
|
123
|
+
passwordConfigured: password.length > 0,
|
|
124
|
+
files: fileEntries,
|
|
125
|
+
totalFiles: fileEntries.length,
|
|
126
|
+
totalBytes,
|
|
127
|
+
staticManifest,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function packageNetlify(argv = process.argv.slice(2)) {
|
|
131
|
+
const projectRoot = process.cwd();
|
|
132
|
+
const args = parseArgs(argv);
|
|
133
|
+
const plan = await buildNetlifyPlan(projectRoot, args);
|
|
134
|
+
if (!args.check) {
|
|
135
|
+
const planPath = path.resolve(projectRoot, LEGACY_PRIVATE_DIR, 'distribution', 'netlify-deploy-plan.json');
|
|
136
|
+
await fs.mkdir(path.dirname(planPath), { recursive: true });
|
|
137
|
+
await fs.writeFile(planPath, `${JSON.stringify(plan, null, 2)}\n`, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
console.log(`[netlify-package] ${plan.dryRun ? 'dry-run ' : ''}${plan.deployDir}: ${plan.totalFiles} files, auth=${plan.authTokenConfigured ? 'set' : 'missing'}, site=${plan.siteIdConfigured ? 'set' : 'missing'}`);
|
|
140
|
+
return plan;
|
|
141
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type StaticOutputMode = 'json-spa' | 'ssg';
|
|
2
|
+
export interface PackageStaticSiteArgs {
|
|
3
|
+
outDir: string;
|
|
4
|
+
check: boolean;
|
|
5
|
+
mode: StaticOutputMode;
|
|
6
|
+
}
|
|
7
|
+
export interface StaticSiteManifest {
|
|
8
|
+
contract: string;
|
|
9
|
+
generatedAt: string;
|
|
10
|
+
deployDir: string;
|
|
11
|
+
outputMode: StaticOutputMode;
|
|
12
|
+
pages: string[];
|
|
13
|
+
requiredFiles: string[];
|
|
14
|
+
contentAssets: string[];
|
|
15
|
+
totalFiles: number;
|
|
16
|
+
totalBytes: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function parsePackageStaticSiteArgs(argv: string[]): PackageStaticSiteArgs;
|
|
19
|
+
export declare function packageStaticSite(argv?: string[], projectRoot?: string): Promise<StaticSiteManifest>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LEGACY_CONFIG_FILE, legacyContract, LEGACY_PRIVATE_DIR } from '../shared/legacyNames.js';
|
|
4
|
+
export function parsePackageStaticSiteArgs(argv) {
|
|
5
|
+
const args = { outDir: 'preview-dist', check: false, mode: 'json-spa' };
|
|
6
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
7
|
+
const arg = argv[index];
|
|
8
|
+
if (arg === '--check') {
|
|
9
|
+
args.check = true;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (arg === '--outDir') {
|
|
13
|
+
args.outDir = argv[index + 1] ?? args.outDir;
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (arg.startsWith('--outDir=')) {
|
|
18
|
+
args.outDir = arg.slice('--outDir='.length);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === '--mode') {
|
|
22
|
+
args.mode = parseStaticOutputMode(argv[index + 1] ?? args.mode);
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg.startsWith('--mode=')) {
|
|
27
|
+
args.mode = parseStaticOutputMode(arg.slice('--mode='.length));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
function parseStaticOutputMode(value) {
|
|
33
|
+
return value === 'ssg' ? 'ssg' : 'json-spa';
|
|
34
|
+
}
|
|
35
|
+
async function fileExists(abs) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = await fs.stat(abs);
|
|
38
|
+
return stat.isFile();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function normalizeRelativePath(value, fallback) {
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return fallback;
|
|
50
|
+
const next = value.trim();
|
|
51
|
+
return next.length > 0 ? next : fallback;
|
|
52
|
+
}
|
|
53
|
+
async function readProjectConfig(projectRoot) {
|
|
54
|
+
const fallback = {
|
|
55
|
+
pagePublishRoot: './src/pages',
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const raw = JSON.parse(await fs.readFile(path.resolve(projectRoot, LEGACY_CONFIG_FILE), 'utf8'));
|
|
59
|
+
if (!isPlainObject(raw))
|
|
60
|
+
return fallback;
|
|
61
|
+
const paths = isPlainObject(raw.paths) ? raw.paths : {};
|
|
62
|
+
return {
|
|
63
|
+
pagePublishRoot: normalizeRelativePath(paths.pagePublishRoot, fallback.pagePublishRoot),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function listFiles(dir) {
|
|
71
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
72
|
+
const files = [];
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (entry.name === '.vite')
|
|
75
|
+
continue;
|
|
76
|
+
const abs = path.join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
files.push(...(await listFiles(abs)));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
files.push(abs);
|
|
82
|
+
}
|
|
83
|
+
return files.sort();
|
|
84
|
+
}
|
|
85
|
+
async function readPageSlugs(projectRoot, config) {
|
|
86
|
+
const pagesRoot = path.resolve(projectRoot, config.pagePublishRoot);
|
|
87
|
+
try {
|
|
88
|
+
const entries = await fs.readdir(pagesRoot, { withFileTypes: true });
|
|
89
|
+
const slugs = [];
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (!entry.isDirectory())
|
|
92
|
+
continue;
|
|
93
|
+
if (await fileExists(path.join(pagesRoot, entry.name, 'index.vue')))
|
|
94
|
+
slugs.push(entry.name);
|
|
95
|
+
}
|
|
96
|
+
return slugs.sort();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function toPosixRelative(root, abs) {
|
|
103
|
+
return path.relative(root, abs).replace(/\\/g, '/');
|
|
104
|
+
}
|
|
105
|
+
async function buildManifest(projectRoot, args) {
|
|
106
|
+
const config = await readProjectConfig(projectRoot);
|
|
107
|
+
const deployRoot = path.resolve(projectRoot, args.outDir);
|
|
108
|
+
const requiredFiles = ['index.html', '404.html'];
|
|
109
|
+
const missing = [];
|
|
110
|
+
for (const fileName of requiredFiles) {
|
|
111
|
+
if (!(await fileExists(path.join(deployRoot, fileName))))
|
|
112
|
+
missing.push(fileName);
|
|
113
|
+
}
|
|
114
|
+
if (missing.length > 0) {
|
|
115
|
+
throw new Error(`[static-package] Missing required deploy files: ${missing.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
const files = await listFiles(deployRoot);
|
|
118
|
+
const totalBytes = (await Promise.all(files.map(async (filePath) => (await fs.stat(filePath)).size))).reduce((sum, size) => sum + size, 0);
|
|
119
|
+
const contentAssets = files
|
|
120
|
+
.map((filePath) => toPosixRelative(deployRoot, filePath))
|
|
121
|
+
.filter((filePath) => filePath.startsWith('__tun' + 'dra/') && filePath.endsWith('/content.json'));
|
|
122
|
+
return {
|
|
123
|
+
contract: legacyContract('static-site-package.v1'),
|
|
124
|
+
generatedAt: new Date().toISOString(),
|
|
125
|
+
deployDir: toPosixRelative(projectRoot, deployRoot),
|
|
126
|
+
outputMode: args.mode,
|
|
127
|
+
pages: await readPageSlugs(projectRoot, config),
|
|
128
|
+
requiredFiles,
|
|
129
|
+
contentAssets,
|
|
130
|
+
totalFiles: files.length,
|
|
131
|
+
totalBytes,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function packageStaticSite(argv = process.argv.slice(2), projectRoot = process.cwd()) {
|
|
135
|
+
const args = parsePackageStaticSiteArgs(argv);
|
|
136
|
+
const manifest = await buildManifest(projectRoot, args);
|
|
137
|
+
if (!args.check) {
|
|
138
|
+
const manifestPath = path.resolve(projectRoot, LEGACY_PRIVATE_DIR, 'distribution', 'static-site-manifest.json');
|
|
139
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
140
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
console.log(`[static-package] ${manifest.deployDir} (${manifest.outputMode}): ${manifest.totalFiles} files, ${manifest.contentAssets.length} content assets`);
|
|
143
|
+
return manifest;
|
|
144
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ServeStaticMode = 'json-spa' | 'ssg';
|
|
2
|
+
export interface ServeStaticSiteArgs {
|
|
3
|
+
host: string;
|
|
4
|
+
mode: ServeStaticMode;
|
|
5
|
+
outDir: string;
|
|
6
|
+
port: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseServeStaticSiteArgs(argv: string[]): ServeStaticSiteArgs;
|
|
9
|
+
export declare function serveStaticSite(argv?: string[], projectRoot?: string): Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const contentTypes = {
|
|
5
|
+
'.css': 'text/css; charset=utf-8',
|
|
6
|
+
'.html': 'text/html; charset=utf-8',
|
|
7
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
8
|
+
'.json': 'application/json; charset=utf-8',
|
|
9
|
+
'.svg': 'image/svg+xml',
|
|
10
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
11
|
+
};
|
|
12
|
+
export function parseServeStaticSiteArgs(argv) {
|
|
13
|
+
const args = {
|
|
14
|
+
host: '127.0.0.1',
|
|
15
|
+
mode: 'json-spa',
|
|
16
|
+
outDir: 'preview-dist',
|
|
17
|
+
port: 4173,
|
|
18
|
+
};
|
|
19
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
20
|
+
const arg = argv[index];
|
|
21
|
+
if (arg === '--host') {
|
|
22
|
+
args.host = argv[index + 1] ?? args.host;
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg.startsWith('--host='))
|
|
27
|
+
args.host = arg.slice('--host='.length);
|
|
28
|
+
if (arg === '--mode') {
|
|
29
|
+
args.mode = parseMode(argv[index + 1] ?? args.mode);
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith('--mode='))
|
|
34
|
+
args.mode = parseMode(arg.slice('--mode='.length));
|
|
35
|
+
if (arg === '--outDir') {
|
|
36
|
+
args.outDir = argv[index + 1] ?? args.outDir;
|
|
37
|
+
index += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg.startsWith('--outDir='))
|
|
41
|
+
args.outDir = arg.slice('--outDir='.length);
|
|
42
|
+
if (arg === '--port') {
|
|
43
|
+
args.port = parsePort(argv[index + 1] ?? String(args.port));
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg.startsWith('--port='))
|
|
48
|
+
args.port = parsePort(arg.slice('--port='.length));
|
|
49
|
+
}
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
function parseMode(value) {
|
|
53
|
+
return value === 'ssg' ? 'ssg' : 'json-spa';
|
|
54
|
+
}
|
|
55
|
+
function parsePort(value) {
|
|
56
|
+
const port = Number(value);
|
|
57
|
+
if (Number.isInteger(port) && port > 0 && port < 65536)
|
|
58
|
+
return port;
|
|
59
|
+
return 4173;
|
|
60
|
+
}
|
|
61
|
+
function decodeRequestPath(req) {
|
|
62
|
+
const rawPath = new URL(req.url ?? '/', 'http://localhost').pathname;
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent(rawPath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return '/';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function requestToCandidates(requestPath) {
|
|
71
|
+
const clean = requestPath.replace(/^\/+/, '');
|
|
72
|
+
if (!clean || clean.endsWith('/'))
|
|
73
|
+
return [path.join(clean, 'index.html')];
|
|
74
|
+
const ext = path.extname(clean);
|
|
75
|
+
return ext ? [clean] : [`${clean}.html`, path.join(clean, 'index.html')];
|
|
76
|
+
}
|
|
77
|
+
function resolveInside(root, relativePath) {
|
|
78
|
+
const resolved = path.resolve(root, relativePath);
|
|
79
|
+
const relative = path.relative(root, resolved);
|
|
80
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
81
|
+
return null;
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
async function fileExists(filePath) {
|
|
85
|
+
try {
|
|
86
|
+
const stat = await fs.stat(filePath);
|
|
87
|
+
return stat.isFile();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function findRequestFile(root, requestPath) {
|
|
94
|
+
for (const candidate of requestToCandidates(requestPath)) {
|
|
95
|
+
const resolved = resolveInside(root, candidate);
|
|
96
|
+
if (resolved && (await fileExists(resolved)))
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
async function sendFile(res, filePath, statusCode) {
|
|
102
|
+
const ext = path.extname(filePath);
|
|
103
|
+
res.writeHead(statusCode, {
|
|
104
|
+
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
105
|
+
'Content-Type': contentTypes[ext] ?? 'application/octet-stream',
|
|
106
|
+
});
|
|
107
|
+
res.end(await fs.readFile(filePath));
|
|
108
|
+
}
|
|
109
|
+
async function handleRequest(root, mode, req, res) {
|
|
110
|
+
const requestPath = decodeRequestPath(req);
|
|
111
|
+
const filePath = await findRequestFile(root, requestPath);
|
|
112
|
+
if (filePath) {
|
|
113
|
+
await sendFile(res, filePath, 200);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const fallback = mode === 'ssg' ? '404.html' : 'index.html';
|
|
117
|
+
const fallbackPath = resolveInside(root, fallback);
|
|
118
|
+
if (fallbackPath && (await fileExists(fallbackPath))) {
|
|
119
|
+
await sendFile(res, fallbackPath, mode === 'ssg' ? 404 : 200);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
|
+
res.end('Not found');
|
|
124
|
+
}
|
|
125
|
+
export async function serveStaticSite(argv = process.argv.slice(2), projectRoot = process.cwd()) {
|
|
126
|
+
const args = parseServeStaticSiteArgs(argv);
|
|
127
|
+
const root = path.resolve(projectRoot, args.outDir);
|
|
128
|
+
if (!(await fileExists(path.join(root, 'index.html')))) {
|
|
129
|
+
throw new Error(`[serve-static] Missing ${args.outDir}/index.html. Build first.`);
|
|
130
|
+
}
|
|
131
|
+
const server = createServer((req, res) => {
|
|
132
|
+
handleRequest(root, args.mode, req, res).catch((error) => {
|
|
133
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
134
|
+
res.end(error instanceof Error ? error.message : 'Internal server error');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
await new Promise((resolve) => {
|
|
138
|
+
server.listen(args.port, args.host, () => {
|
|
139
|
+
console.log(`[serve-static] ${args.mode} ${args.outDir} -> http://${args.host}:${args.port}/`);
|
|
140
|
+
resolve();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -315,16 +315,7 @@ const CORE_DESIGN_OP_CONFIG_RAW = {
|
|
|
315
315
|
step: 0.5,
|
|
316
316
|
},
|
|
317
317
|
private: true,
|
|
318
|
-
resolve: (v) =>
|
|
319
|
-
const numericValue = Number(String(v));
|
|
320
|
-
if (!Number.isFinite(numericValue)) {
|
|
321
|
-
return Emit.isEmpty(v) ? 'border-t' : `border-t-(length:--border-${String(v)})`;
|
|
322
|
-
}
|
|
323
|
-
if (Number.isInteger(numericValue)) {
|
|
324
|
-
return `border-t-${numericValue}`;
|
|
325
|
-
}
|
|
326
|
-
return `border-t-[${numericValue}px]`;
|
|
327
|
-
},
|
|
318
|
+
resolve: (v) => Emit.borderSide('t', v),
|
|
328
319
|
},
|
|
329
320
|
borderWidthR: {
|
|
330
321
|
tokenFamily: 'borderWidth',
|
|
@@ -336,16 +327,7 @@ const CORE_DESIGN_OP_CONFIG_RAW = {
|
|
|
336
327
|
step: 0.5,
|
|
337
328
|
},
|
|
338
329
|
private: true,
|
|
339
|
-
resolve: (v) =>
|
|
340
|
-
const numericValue = Number(String(v));
|
|
341
|
-
if (!Number.isFinite(numericValue)) {
|
|
342
|
-
return Emit.isEmpty(v) ? 'border-r' : `border-r-(length:--border-${String(v)})`;
|
|
343
|
-
}
|
|
344
|
-
if (Number.isInteger(numericValue)) {
|
|
345
|
-
return `border-r-${numericValue}`;
|
|
346
|
-
}
|
|
347
|
-
return `border-r-[${numericValue}px]`;
|
|
348
|
-
},
|
|
330
|
+
resolve: (v) => Emit.borderSide('r', v),
|
|
349
331
|
},
|
|
350
332
|
borderWidthB: {
|
|
351
333
|
tokenFamily: 'borderWidth',
|
|
@@ -357,16 +339,7 @@ const CORE_DESIGN_OP_CONFIG_RAW = {
|
|
|
357
339
|
step: 0.5,
|
|
358
340
|
},
|
|
359
341
|
private: true,
|
|
360
|
-
resolve: (v) =>
|
|
361
|
-
const numericValue = Number(String(v));
|
|
362
|
-
if (!Number.isFinite(numericValue)) {
|
|
363
|
-
return Emit.isEmpty(v) ? 'border-b' : `border-b-(length:--border-${String(v)})`;
|
|
364
|
-
}
|
|
365
|
-
if (Number.isInteger(numericValue)) {
|
|
366
|
-
return `border-b-${numericValue}`;
|
|
367
|
-
}
|
|
368
|
-
return `border-b-[${numericValue}px]`;
|
|
369
|
-
},
|
|
342
|
+
resolve: (v) => Emit.borderSide('b', v),
|
|
370
343
|
},
|
|
371
344
|
borderWidthL: {
|
|
372
345
|
tokenFamily: 'borderWidth',
|
|
@@ -378,16 +351,7 @@ const CORE_DESIGN_OP_CONFIG_RAW = {
|
|
|
378
351
|
step: 0.5,
|
|
379
352
|
},
|
|
380
353
|
private: true,
|
|
381
|
-
resolve: (v) =>
|
|
382
|
-
const numericValue = Number(String(v));
|
|
383
|
-
if (!Number.isFinite(numericValue)) {
|
|
384
|
-
return Emit.isEmpty(v) ? 'border-l' : `border-l-(length:--border-${String(v)})`;
|
|
385
|
-
}
|
|
386
|
-
if (Number.isInteger(numericValue)) {
|
|
387
|
-
return `border-l-${numericValue}`;
|
|
388
|
-
}
|
|
389
|
-
return `border-l-[${numericValue}px]`;
|
|
390
|
-
},
|
|
354
|
+
resolve: (v) => Emit.borderSide('l', v),
|
|
391
355
|
},
|
|
392
356
|
borderColor: {
|
|
393
357
|
tokenFamily: 'color',
|
|
@@ -528,13 +492,7 @@ const CORE_DESIGN_OP_CONFIG_RAW = {
|
|
|
528
492
|
step: 0.001,
|
|
529
493
|
},
|
|
530
494
|
private: true,
|
|
531
|
-
resolve: (v) =>
|
|
532
|
-
const numericValue = Number(String(v));
|
|
533
|
-
if (Number.isFinite(numericValue)) {
|
|
534
|
-
return `leading-[${numericValue}]`;
|
|
535
|
-
}
|
|
536
|
-
return Emit.prefix('leading', v);
|
|
537
|
-
},
|
|
495
|
+
resolve: (v) => Emit.leading(v),
|
|
538
496
|
},
|
|
539
497
|
tracking: {
|
|
540
498
|
tokenFamily: 'tracking',
|